编码的过程就是从高维信息扁平化到线性空间的过程;
执行的过程就是从线性空间展开到高维空间的过程。
在页目录及页表划分中,每一页指向4K空间。
在分段中,当G=1时,段长度的颗粒度为4K。分段与分页的颗粒度是一致的。都可以实现4K边界对齐。
段长度 | 20位,与G标志位一起使用 |
---|---|
段基地址 | 32位,可指向保护模式支持最大空间4GB任意位置 |
S | 复位为系统段,置位为code/data段 |
DPL | 描述符特权级 |
P | 表示该段是否在内存中,1为在内存中 |
AVL | 设为0即可 |
L | 保留使用,设为0即可 |
D/B | 代码段数据位宽,1为32位,0为16位 |
G | 指定段长度的颗粒度,1为4kb为颗粒度(段长为4GB),0为字节颗粒度(段长为1MB) |
TYPE | 占4位,对S标志位进一步解释 |
分段与分页,分的都是4G线性地址空间。分段是从逻辑层面进行的划分;分页是从存储层面进行的划分。
以上为windows的段描述符表。
进入保护模式之前,会使用临时GDT
gdt:
.fill GDT_ENTRY_BOOT_CS, 8, 0
.word 0xFFFF# 4Gb - (0x100000*0x1000 = 4Gb)
.word 0# base address = 0
.word 0x9A00# code read/exec
.word 0x00CF# granularity = 4096, 386# (+5th nibble of limit)
.word 0xFFFF# 4Gb - (0x100000*0x1000 = 4Gb)
.word 0# base address = 0
.word 0x9200# data read/write
.word 0x00CF# granularity = 4096, 386# (+5th nibble of limit)
gdt_end:
这里只定义了两个DPL为0的代码段和数据段,只给内核使用的。
设置真正的IDT和GDT
lgdt cpu_gdt_descr // 真正的GDT
lidt idt_descr //真正的IDT
ljmp $(__KERNEL_CS),$1f // 重置CS
1: movl $(__KERNEL_DS),%eax
# reload all the segment registers
movl %eax,%ss # after changing gdt. // 重置SSmovl
$(__USER_DS),%eax
# DS/ES contains default USER segment
movl %eax,%ds
movl %eax,%es
xorl %eax,%eax
# Clear FS/GS and LDT
movl %eax,%fs
movl %eax,%gs
lldt %ax cld # gcc2 wants the direction flag cleared at all times// push一个假的返回地址以满足 start_kernel()函数 return的要求
pushl %eax # fake return address
对于GDT我们最关心的__KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS这4个段描述符:
.quad0 x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */
.quad0 x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */
.quad0 x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
.quad0 x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */
0号进程代表整个linux内核且每个CPU有一个。
NASM语法
$表示"此处的地址"。
$$表示"当前段的起始地址"。
因此$-$$表示"当前节的大小"。
下述用法适用于只有一个段的情况,在有多个段比如数据段时不准确。
times 510-($-$$) db 0
db 0x55,0xaa
段寄存器是根据内存分段的管理模式来设定的。内存单元的物理地址由段寄存器的值和一个偏移量组合而成,这样可以用两个较小位数的值组合成一个可访问的较大物理空间的内存地址。
32位寄存器
32位CPU所含有的寄存器有:
8个32位通用寄存器,其中包含4个数据寄存器(EAX、EBX、ECX、EDX)、2个变址寄存器(ESI和EDI)和2个指针寄存器(ESP和EBP)
6个段寄存器(ES、CS、SS、DS、FS、GS)
1个指令指针寄存器(EIP)
1个标志寄存器(EFLAGS)
如下图所示:
CS高13位0000 0000 0010 0b
DS/ES/SS/GS高13位0000 0000 0010 1b
FS高13位0000 0000 0101 0b
CPU内部的段寄存器:
CS——代码段寄存器(CodeSegmentRegister),其值为代码段的段值;
DS——数据段寄存器(DataSegmentRegister),其值为数据段的段值;
ES——附加段寄存器(ExtraSegmentRegister),其值为附加数据段的段值;
SS——堆栈段寄存器(StackSegmentRegister),其值为堆栈段的段值;
FS——附加段寄存器(ExtraSegmentRegister),其值为附加数据段的段值;
GS——附加段寄存器(ExtraSegmentRegister),其值为附加数据段的段值。
在16位系统中,只有4个段寄存器,所以,程序在任何时刻至多有4个正在使用的段可直接访问;
在32位系统中,它有6个段寄存器,所以,在此环境下开发的程序最多可同时访问6个段。
在32位CPU中段寄存器有什么作用?
理想情况
32位cpu 地址线扩展成了32位,这和数据线的宽度是一致的。
因此,在32位机里其实并不需要采用“物理地址=段:偏移”这种地址表达方式。
原来在16位机里规定的每一个段不大于64kb在32位机里也不是必要的。
所以,对于32位机来讲,最简单的方法就是用一个32位数来标识一个字节的存储地址,寻址时只要给出一个32位数就可以直接找到地址。
这种地址储存模型就属于“平展储存模型”。
实模式的由来
但是,新产品一般都希望遵循“向下兼容”这个原则。所以,32位机里完整的保留了16位寻址模式。即:寻址能力为1M;分段机制;每段不超过64kb。这就是通常所说的”实模式”。在地址储存模型中属于“实地址储存模型”。
32位CPU中16位的段寄存器
考虑到程序通常都是功能化的模块,所以分段虽然不是必要的,但分段却能大大提高编程者管理程序的效率。故而32位机也采用了段+偏移的模式来寻址。
但与实模型不同的是,由于地址线和数据线宽度一致,因而,每个段最大可以到4G,并且段基址也是32位的无需进行左移处理。
在地址储存模型中这属于“段地址储存模型”。
然而需要注意的是,在32位机里,虽然通用寄存器,标志寄存器等都扩展成了32位,但是段寄存器却依然是16位的(为什么不做改变??我猜可能是这样便于向下兼容)。
所以在32位寻址时,段寄存器里放的不再是段基址(位数不够,放不下),而是一个选择子。这个选择子对应了一个64-bit长的描述符,64-bit的描述符里有32-bit是段基址。
所以原来在16位机里通过段寄存器一步就可以找到段基址,而现在在32位机里分成了两步:先找选择子,然后通过选择子找段基址。段基址找到了,再加上偏移地址,物理地址就找到了。
段寄存器的其他属性
在32位机里,“段”有三个要素:基地址,长度,属性(属性里包含了优先级和其它的一些内容)。
为了能一次完整的引用或者给出这三个要素,需要新定义一个数据结构。
这个结构就是前面所提到的描述符,每一个描述符都占有64-bit,有足够的长度来包含段的三个要素。
当然,现在内存中不止一个程序,而且程序也不止一个段,所以描述符也不会有一个,而是很多个。
最简单的管理方法,就是将所有的描述符集中起来放在一块连续的存储空间里,然后给各个描述符排上序号。
当要找某一个特定的描述符时,只要先找到这块连续的存储空间然后给出序号就可以了。
这些集中起来的描述符形成了一张表,所以通常被叫做描述符表。
所以,想找到一个段的信息,首先要找到描述符表。也就是说,找特定的描述符先要知道描述符表的基地址。在32位cpu中,有一个48位的专用寄存器用来存放全局描述符表的信息,这个寄存器叫做GDTR。
其中,高32位给出了全局描述符表的基地址,低16位给出了描述符表的长度限制。
所以,一张全局描述符表最长可以是64kb。那么,最多可以放64kb/8b=8k个描述符。
所以如果想在其中选择任意一个描述符,用13位就可以办到了。
在32位cpu中,16位段寄存器的高13位就用来存放特定描述符的序号(本句是保护模式分段机制的点睛之笔,最近几天的搜寻只此一句)。
其实,现在段寄存器的功能就是选择描述符,正因为如此,通常也把段寄存器叫做选择器。
分页机制
它就是把内存分成一个一个连续的页,每页大小4kb。与段不同,页不是程序功能块的体现。
一个程序功能块可能占用好多个页。现在内存就像一本书了,一页一页的,每页的容量都是相等的。当然,我们很快可以联想到,要想能够很快的找到某页,最好给这本书分个章或者节什么的,然后逐级地向下查询。这就是32位cpu里页目录和页表所起的作用。
页目录的长度是4kb,它最多可以包含1024个页目录项,每个页目录项32-bit,包含了页表的地址和有关信息。所以,页目录把4Gb空间分成了1024个页组,每个页组4MB的大小。
页表的长度也是4kb,1024个页表项,每个页表项32- bit,包含页的地址和其它信息。这样,4MB的页组又被分成了1024个页面,每个页面大小4kb。
所以找到某一个页就是先查页目录再查页表这么一个过程。
为了找到页目录,我们需要知道其基地址。
在32位cpu里,CR3寄存器里高20位放的就是页目录的地址,因为页目录的低12位总是0,这样保证页目录始终是页对齐的(每页大小4kB)。
再来看一看前面通过32位段机制找到的线性地址。其高10位是页目录的偏移地址,一共1024个页目录用10位就可以标识可能的最大偏移了。
加上CR3,就可以找到页目录,然后再通过页目录找到页表的基地址,线性地址的中间10位放的是页表的偏移量,这样就找到了页表。
最后页表的基地址再加上CR3最低12位所表征的页表的偏移地址就找到了页,这个页的地址就是最终的物理地址。