综合四篇文章看下分段与分页机制:
分段:LDT与GDT
分页:页目录表与页表
首先明确两个结论:
1.cr3里保存页目录表的基址的地址类型为物理地址,页目录表里的每一项也是页表的物理地址。
2.gdtr的地址和gdt里的描述符里的地址类型一样,保护模式开启后都为线性地址,但在保护模式开启前放的确是物理地址。系统开机时,进入的实模式,这个时候如果想进入保护模式,必须先设置GDT表,所以首先是在实模式设置GDT表为进入保护模式做准备,这个时候当然用的都是物理地址。但进入保护模式后,分段启用,那些设置的物理地址就被当成线性地址使用了,所以GDT要设置成使得物理地址和线性地址等价。就linux来说,进入保护模式后利用实模式设置的那个临时GDT,开启分页,以后又再次重新设置了新的GDT,这个GDT就全用的线性地址了。
可以看出要动态地来看待寄存器中要放什么样的值,在CPU不同的工作模式下,有些寄存器的的值是需要重新设置。寄存器的值并不是一次性设置后就不会需要重新设置的。
《分段与分页,LDT与GDT》
原文链接:https://blog.csdn.net/yleek/article/details/8204393
一个进程的地址空间,从用户的角度看,是由若干的段(segment)组成的,这些段可以分为两种:私有段(private)、共享段(shared)。cpu也是按照用户的逻辑进行内存管理的(分段),Intel Pentium规定了每种段最多有8K个,每个segment最大4G。一个cpu对应有一个GDT(global descriptor table),该表详细描述了shared segment,这个表为所有进程共享的;一个process对应有一个LDT(local),该表详细描述了该process的private segment,这个表是进程私有的。
GDT/LDT的结构,每条记录有64bit:
硬件中有一个寄存器,叫做GDTR(registor),共48bit,如下:
注意:上面16bit规定了GDT的长度,该长度是指,从32bit的指针指向的GDT开始位置到GDT结束位置的绝对长度,这个长度不是表的长度,不是表的record数量。下面会说明这个绝对长度跟表的长度(表的record数量)的关系。
文章开头说了,Intel Pentium规定了每种段最多有8K个,也就是说每个GDT/LDT有8K(213)条record,GDTR中又规定了GDT的绝对长度是216,由此可以得出GDT中每个record长度为2^16 / 2^13 = 8Byte,共64bit,正好与上面GDT/LDT的结构中说明的相一致。
问题又来了,既然每个GDT/LDT可以有2^13条record,那么就需要一个至少13bit的Selector来确定是哪一条record了。事实也是这样的,一个逻辑地址包含两部分,如下:
由上可以发现,32bit的段内偏移量确实确定了每个段的大小最大为4G(如文章开头所述)。而Selector是16bit不是我们预测的13bit,事实上其结构如下:
Segment Selector的结构
由以上介绍可知一个逻辑地址如何转化成为线性地址(确实可以得到一个32bit的地址,先就叫他线性地址吧,其实这个32bit的地址还要分页的)。为什么一个逻辑地址是48bit而不是32bit呢(如表“逻辑地址结构”所示)?我们在程序中如果打印一个变量的地址,确实就是一个32bit的数字啊,类似0x12345678,它并不是48bit的啊?!别忘了,我们的程序是处在一个个的Segment中的(如文章开始所述),无论是指令还是数据。我们打印出的地址其实是段内的偏移地址,程序的16bit段号由操作系统分配管理,我们是看不见的,但是的确存在。
假设一个进程需要1M的内存(指令加数据),那么os先会从LDT中选择一个段号(16bit),分给这个进程(在这个段内,我们有4G的空间可以发挥,但是我们只需要1M _),如果进程有需要全局空间,os还会在GDT中为之分配的。因此…cpu寻址的时候,也需要根据段号加段内偏移地址来找到线性地址。
注意注意注意:以上的解释中,为简单,没有说明分页!
总之,SegmentSelector(16bit) + 段内Offset(32bit) = 一个32bit的地址值
下面我告诉你,我们要对刚刚得到的32bit地址值进行分页处理,下面所有的操作都是在一个Segment内的4G空间内进行的。分页是这样分的:
操作系统为每个Segment维护一张表,称作页表,结构如下:
一页大小有页内偏移量就可以看出,是4K,如此为了管理4G的空间,显然这个页表需要有220条record。一条记录32bit=4Byte,220条就得需要4MB的空间啊!怎么能接受!处理的方法就是在此对0-19bit进行分页,新的页表结构如下:
二次分页的页表结构
4G空间一共有1024x1024页,第一次分页把这1024x1024页分成1024组,每组1024个,这样就需要1024张表,每张表1024条record……接着第二次分组,我们用一张表来管理这1024张表,显然着这张表也有1024条记录,如此一共有1+1024帐表,os会把不用的表交换到外存,以节省内存空间,提高查找效率。
综上所述,在程序中新申请一块空间,大致过程是这样的:
---->os在LDT中分配一个段号,在这个段里有4G的空间;
---->把这4G空间先分成1024份,每份大小1024 x 4K
---->把每一分再分为1024份,每份大小4K
---->把其中的一份或几份分配给申请者,并记录。
————————————————
《两张图看懂GDT、GDTR、LDT、LDTR的关系》
原文链接:https://blog.csdn.net/weixin_46198176/article/details/120248319
段选择符
32位汇编中16位段寄存器(CS、DS、ES、SS、FS、GS)中不再存放段基址,而 是段描述符在段描述符表中的索引值,D3-D15位是索引值,D0-D1位是优先级(RPL)用于特权检查,D2位是描述符表引用指示位TI,TI=0指 示从全局描述表GDT中读取描述符,TI=1指示从局部描述符中LDT中读取描述符。这些信息总称段选择符
段描述符
8个 字节64位,每一个段都有一个对应的描述符。根据描述符描述符所描述的对象不同,描述符可分为三类:储存段描述符,系统段描述符,门描述符(控制描述 符)。在描述符中定义了段的基址,限长和访问内型等属性。其中基址给出该段的基础地址,用于形成线性地址;限长说明该段的长度,用于存储空间保护;段属性 说明该段的访问权限、该段当前在内存中的存在性,以及该段所在的特权级。
S: 描述符类型(0=系统段,1=数据段或者代码段)
TYPE: 段类型,包括数据段和代码段
整个段描述符包含三部分段基地址,三部分组合得到保护模式下完整的32位段基址。
段描述符表
IA-32处理器把所有段描述符按顺序组织成线性表 放在内存中,称为段描述符表。分为三类:全局描述符表GDT,局部描述符表LDT和中断描述符表IDT。GDT和IDT在整个系统中只有一张,而每个任务 都有自己私有的一张局部描述符表LDT,用于记录本任务中涉及的各个代码段、数据段和堆栈段以及本任务的使用的门描述符。GDT包含系统使用的代码段、数 据段、堆栈段和特殊数据段描述符,以及所有任务局部描述符表LDT的描述符。
GDTR全局描述符寄存器
48位,高32位存放GDT基址,低16为存放GDT限长。
LDTR局部描述符寄存器
16位,高13为存放LDT在GET中的索引值。
保护模式下的内存寻址
IA-32处理器仍然使用段选择器、偏移量逻辑方式表示一个线性地址,那么是怎么得到段的基址呢?在上面说明中我们知道,要得到段的基址首先通过段选择符段选择器中TI位指定的段描述符所在位置: 当 TI=0时表示段描述符在GDT中,如下图所示:
① 先从GDTR寄存器中获得GDT基址。
② 然后再GDT中以段选择符高13位位置索引值得到段描述符。
③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址(程序给出)才得到最后的线性地址。
当TI=1时表示段描述符在LDT中,如下图所示:
① 还是先从GDTR寄存器中获得GDT基址。
② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。
③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
④ 用段选择符高13位位置索引值从LDT段中得到段描述符。
个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
④ 用段选择符高13位位置索引值从LDT段中得到段描述符。
⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址(程序给出)才得到最后的线性地址。
————————————————
《Linux系统文件页表目录和页表结构(图文详解)》
https://zhuanlan.zhihu.com/p/429914858
两级页表如何实现地址转换:
页表:是一种特殊的数据结构,记录着页面和页框的对应关系。(映射表)
页表的作用:是内存非连续分区分配的基础,实现从逻辑地址转化成物理地址。
(1) 按照地址结构将逻辑地址拆成三个部分。
(2) 从PCB中读取页目录起始地址,再根据一级页号查页目录表,找到下一级页表在内存中存放位置。
(3) 根据二级页号查表,找到最终想要访问的内存块号。
(4) 结合页内偏移量得到物理地址。
虚拟存储技术
再解决了页必须连续存放的问题后,再看如何第二个问题:没有必要让整个页表常驻内存,因为进程一段时间内可能只需要访问某几个特定的页面。
解决方案:可以在需要访问页面时才把页面调入内存——虚拟存储技术(后面再说)。可以在页表中增加一个标示位,用于表示该页表是否已经调入内存。
应用
若采用多级页表机制,则各级页表的大小不能超过一个页面。
举例说明,某系统按字节编址,采用40位逻辑地址,页面大小为4KB,页表项大小为4B,假设采用纯页式存储,则要采用()级页表,页内偏移量为()位?
页面大小 = 4KB,按字节编址,因此页内偏移量为12位。
页号 = 40 - 12 = 28位。
页面大小 = 4KB,页表项大小 = 4B,则每个页面可存放1024个页表项。因此各级页表最多包含1024个页表项,需要10个二进制位才能映射到1024个页表项,因此每级页表对应的页号应为10位二进制。共28位的页号至少要分为3级。
1、 进程的4G 线性空间被划分成三个部分:进程空间(0-3G)、内核直接映射空间(3G – high_memory)、内核动态映射空间(VMALLOC_START - VMALLOC_END)
2、 三个空间使用同一张页目录表,通过 CR3 可找到此页目录表。但不同的空间在页目录表中页对应不同的项,因此互相不冲突
3、 内核初始化以后,根据实际物理内存的大小,计算出 high_memory、VMALLOC_START、VMALLOC_END 的值。并为“内核直接映射”空间建立好映射关系,所有的物理内存都可以通过此空间进行访问。
4、 “进程空间”和“内核动态映射空间”的映射关系是动态建立的(通过缺页异常)
假设在有三个线性地址 addr1, addr2, addr3 ,分别属于三个线性空间不同部分(0-3G、3G-high_memory、vmalloc_start-vmalloc_end),但是最终都映射到物理页面1:
1、 三个地址对应不同的页表和页表项
2、 但是页表项的高 20bit 肯定是1,表示物理页面的索引号是1
3、 同时,根据高 20 bit,可以从 mem_map[] 中找到对应的 struct page 结构,struct page 用于管理实际的物理页面(就是实际物理页面的物理地址了,到这里就不绕弯子了,顺便想到高速缓冲的匹配命中操作是用哈希表,换算出的要访问的实际物理地址拿到哈希表的输入计算一下哈希值,看看有没命中)(红线)
4、 从线性地址最终的,根据页目录表,页表,可以找到物理地址
5、 Struct page 和物理地址之间很容易互相转换
6、 从物理地址,可以很容易的反推出在内核直接映射空间的线性地址(蓝线)。要想得到在进程空间或者内核动态映射空间的对应的线性地址,则需要遍历相应的“虚存区间”链表。
关于页目录表:
1、 每个进程有一个属于自己的页目录表,可通过 CR3 寄存器找到
2、 而内核也有一个独立于其它进程的页目录表,保存在 swapper_pg_dir[] 数组中
3、 当进程切换的时候,只需要将新进程的页目录把地址加载到 CR3 寄存器中即可
4、 创建一个新进程的时候,需要为它分配一个 page,作为页目录表,并将 swapper_pg_dir[] 的高 256 项拷贝过来,低 768 项则清0
linux0.11版本,所有进程共享同一个页目录而各自使用不同的页表,该共享的页目录就放在物理地址最前面的4k
《关于gdtr和cr3地址类型的理解》
https://blog.csdn.net/qq_33439820/article/details/79012896
结论:1.cr3里保存页目录表的基址的地址类型为物理地址,页目录表里的每一项也是页表的物理地址。
2.gdtr里保存的地址类型为线性地址。
原因:由于段表并不能保证页表存在或开启,所以它的机制,完全建立在无页表存在的情况。体现在gdtr上,就是gdtr的地址和gdt里的描述符里的地址类型一样,都为线性地址,当开启分页机制后有可能会和物理地址不同。而且,当想要更换段页式的时候,必须在现有地址转换情况下,构造段表和页表,但是当往gdtr里填段表基址是,却必须是在无段表转换的地址。页表和页目录表的基址是无段表和页表转换后的地址,也就是物理地址。
换句话说,就是换段表时,不依赖现有段表。换页表时,不依赖段表和页表。
比如下面这张图,好像是intel手册上的,但是根据我的实践,不知道是不是我的理解不对,我认为不仅cr3是物理地址,页目录表项和页表项都应该是物理地址。
题外话:最近,想实现一个简单的基于x86的操作系统内存管理功能,网上关于段页式内存管理介绍也挺多,但是,由于自己对计算机硬件不是很了解,所以,在算法之余,更多问题是硬件的细节问题。感觉如果硬件细节不清楚,总会遇到很多奇怪的问题,而且,很难排除,花了很多时间,心里也没底。所以也建议大家无论做什么,基础永远值得花更多时间。
————————————————
《linux 线性地址 物理地址,GDTR和LDTR中放置的是物理地址还是线性地址?》
https://blog.csdn.net/weixin_39945531/article/details/116878299
系统启动时候要设置GDTR和LDTR,它们使用的是线性地址还是物理地址呢?如果是物理地址,是不是说启动到这个时候,分页机制并没有开启,也就是说先转换到32位保护模式,不过是段式的?如果是线性地址的话,是不是说之前就要先设置好页目录和页表,然后装入它们,开启32位保护模式就是直接开启了分页机制?
|
系统开机时,进入的实模式,这个时候如果想进入保护模式,必须先设置GDT表,所以首先是在实模式设置GDT表
为进入保护模式做准备,这个时候当然用的都是物理地址,但进入保护模式后,分段启用,那些设置的物理地址
就被当成线性地址使用了,所以GDT要设置成使得物理地址和线性地址等价,就linux来说,进入保护模式后利用
实模式设置的那个临时GDT,开启分页,以后又再次重新设置了新的GDT,这个GDT就全用的线性地址了。
|
实模式何须要GDT/LDT等内存管理/分页分段的产物。
但是,GDT(boot_GDT)的建立是在实模式阶段进行的。
当然,GDT register里面的是线性地址了。
当real mode to protected mode时候,需要设定PDBR(cr3)的值,加载gdt(线性地址)。
每个process有一个gdt的copy,then they everyone can seperately modify their copy gdt.
ldt is build up after gdt.
the following codes are the codes what the real mode to protected mode.
real_to_prot:
.code16
cli
/* load the GDT register ///************************/
DATA32ADDR32lgdtgdtdesc //load line address
/* turn on protected mode */
movl%cr0, %eax
orl$GRUB_MEMORY_MACHINE_CR0_PE_ON, %eax
movl%eax, %cr0
/* jump to relocation, flush prefetch queue, and reload %cs */
DATA32ljmp$GRUB_MEMORY_MACHINE_PROT_MODE_CSEG, $protcseg
.code32
protcseg:
/* reload other segment registers */
movw$GRUB_MEMORY_MACHINE_PROT_MODE_DSEG, %ax
movw%ax, %ds
movw%ax, %es
movw%ax, %fs
movw%ax, %gs
movw%ax, %ss
/* put the return address in a known safe location */
movl(%esp), %eax
movl%eax, GRUB_MEMORY_MACHINE_REAL_STACK
/* get protected mode stack */
movlprotstack, %eax
movl%eax, %esp
movl%eax, %ebp
/* get return address onto the right stack */
movlGRUB_MEMORY_MACHINE_REAL_STACK, %eax
movl%eax, (%esp)
/* zero %eax */
xorl%eax, %eax
/* return on the old (or initialized) stack! */
ret