《Linux0.11源码解读》理解(五) head之开启分页

文章详细介绍了CPU从16位实模式向32位保护模式演进的过程,特别是32位保护模式下的分页机制。在32位模式下,通过段寄存器和全局描述符表进行寻址,并使用分页将逻辑地址转换为物理地址。文章还展示了如何初始化页目录和页表,以及如何通过CR0寄存器开启分页功能,最终实现对4GB虚拟内存的管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先回顾一下地址长度以及组合的演变:16位cpu意味着其数据总线/寄存器也是16位,但是地址总线(寻址能力)与此无关,可能是20位。可以参考:cpu的位宽、操作系统的位宽和寻址能力的关系_cpu位宽_brahmsjiang的博客-优快云博客

也就因为寄存器和地址总线位数的不对等,于是16位cpu的寻址方式是: 物理地址=段地址×16+偏移地址。这叫做实模式。但随着历史的演进,cpu已经支持32位(地址总线可以36位)、甚至64位(地址总线可以40位)。为了提升寻址能力和安全性,也为了兼容16位系统,在setup阶段就开始准备从16位实模式往32位保护模式做准备。

在32位保护模式下,段寄存器中存储是段选择子。根据段选择子的索引去查询全局描述符表以获得段描述符,其中存储着基地址物理地址=基地址+偏移地址

一旦开启了分段机制,还要多一步: 

 

程序员写代码时给出的地址叫逻辑地址,其中包含段选择子和偏移地址两部分。通过分段后得到的叫线性地址,线性地址通过分页后得到的叫物理地址。根据前一节,我们得到的线性地址是32位(gdt中每一项的基地址是32bit+偏移地址),分页机制大概是这样运作的:

线性地址被分为高 10 位、中间 10 位、后 12 位。高 10 位在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。这个过程是由硬件MMU(内存管理单元)完成的。

现在我们继续上一节,将head程序跳转至setup_paging开启分页:

setup_paging:
    mov ecx,1024*5    ;用来计数, 5 page: pg_dir + 4 page tabl
    xor eax,eax       ;eax清零
    xor edi,edi       ;edi清零
    pushf             ;保存所有标志, 这里主要为了DF(方向标志位)
    cld               ;让DF=0,用于串操作指令中。决定内存地址递增
    rep stosd         ;重复执行后面的指令stos。每次执行时从ecx-1,直到ecx=0则结束重复
                      ;是初始化大块内存最快的方法
    mov eax,_pg_dir   ;设置页目录中的项,仅需4个。_pg_dir为页表目录标号(0地址开始)
    mov [eax],pg0+7   ;pg0+7表示:0x00001007,是页目录表中的第1项
    mov [eax+4],pg1+7 ;mov [eax],xxx: 间接寻址,即eax指向内容填充为xxx,非eax=xxx
    mov [eax+8],pg2+7
    mov [eax+12],pg3+7
    mov edi,pg3+4092
    mov eax,00fff007h ;16Mb-4096+7 (r/w user,p)
    std
L3: stosd             ;将32位的eax的内容复制到es:edi指向的内存空间,并自动将edi的内容加4
    sub eax,00001000h
    jge L3
    popf
    xor eax,eax
    mov cr3,eax       ;设置页目录基址寄存器cr3的值,指向页目录表,在0x0000处
    mov eax,cr0
    or  eax,80000000h
    mov cr0,eax       ;开启分页,cr0的PG标志
    ret

上述代码的意义就是在内存中一次写好页目录表和页表,之后开启cr0寄存器的分页开关。

首先按当前分页机制:

页目录项有10位,最多可以指示1K项的页表
页表项也有10位,最多可以指示1K项的页起始地址
线性地址低12位,最多可以指示4K偏移地址,4K为一个页。
分页后总共可访问的虚拟内存空间为:1024*1024*4K=4G。但当时Linux 0.11认为,总共可使用的内存不会超过16M。于是4(页表数)* 1024(页表项数) * 4KB(一页大小)= 16MB,即1个页目录表 + 4个页表就能指示全部的16M内存。这也就是ecx计数器设为1024*5的原因,意在通过rep stosd将5个页结构全部清0即可。

接着填写页目录中的项,页目录是第0项,除去页目录我们共有4个页表只需设置4项(页目录或者每个页表项本身也占据4K,每项4byte,一共可以设置1024个项/条目)。来看mov [eax],pg0+7:

pg_dir:            ;页目录在0地址处,会覆盖掉执行过的代码
.globl startup_32
startup_32:
...


.org 0x1000        ;从0X1000开始存放内存页表pg0,内存页表pg0长度4KB,寻址跨度4K(FFFh)
pg0:               ;页表pg0开始出添加一个标号pg0,方便后面的赋值操作。

.org 0x2000        ;从0X2000开始存放内存页表pg1,内存页表pg1长度4KB,寻址跨度4K(FFFh)
pg1:               ;页表pg0开始出添加一个标号pg1,方便后面的赋值操作。

.org 0x3000        ;从0X3000开始存放内存页表pg2,内存页表pg2长度4KB,寻址跨度4K(FFFh)
pg2:               ;页表pg2开始出添加一个标号pg2,方便后面的赋值操作。

.org 0x4000        ;从0X4000开始存放内存页表pg3,内存页表pg3长度4KB,寻址跨度4K(FFFh)
pg3:               ;页表pg3开始出添加一个标号pg3,方便后面的赋值操作。

.org 0x5000        ;定义下面的内存数据块从偏移0x5000 处开始

回顾下在实模式下,ds基地址,eax偏移地址所代表的内存单元:ds*16+eax
在保护模式下,ds段选择子,如0x10指向的是全局描述符表中的第二个段描述符(数据段描述符),里面内容中的段基址是 0。exa偏移地址所代表的内存单元:0+eax (这里+4/+8/+12是因为每一项4字节,32位)。

而页目录项结构与页表中项结构一样,页目录表中的第1项赋值为pg0+7即0x00001007,则
页目录表中的第1项的页表地址= 0x00001007 & 0xfffff000 = 0x1000
页目录表中的第1项的属性标志 = 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写。
以此类推,我们用此方法把页目录表中的第2、3、4项的地址、属性给设置了。

接着填写页表(非页目录)中的项,共有:4(页表)*1024(项/页表)=4096 项(0 - 0xfff),也即能映射物理内存4096*4K = 16M。
每项内容是:当前项映射的实际物理内存地址+该页的标志。我们从最后一个页表的最后一项始按倒退填写。一个页表的最后一项在页表中的偏移地址是1023*4 = 4092(表项从0到1023,偏移地址=序号*4)。因此最后一页页表的最后一项的位置就是$pg3+4092。这里edi作为stosd的目的地址被写入,充当了地址变量的作用。然后作为16M内存的最后一页的物理地址是16Mb - 4096 = 0xfff000,加上属性标志7,即为0xfff007。这里eax作为stosd的写入内容,每次写入递减0x1000。

等上述内容全部写入完毕,设置页目录基址寄存器cr3的值,即在0x0000处。然后开启分页(cr0 的PG 标志,位31)。大功告成!大概整个内存效果图是这样:

接下来终于可以进入main.c了! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值