先回顾一下地址长度以及组合的演变:16位cpu意味着其数据总线/寄存器也是16位,但是地址总线(寻址能力)与此无关,可能是20位。可以参考:cpu的位宽、操作系统的位宽和寻址能力的关系_cpu位宽_brahmsjiang的博客-CSDN博客
也就因为寄存器和地址总线位数的不对等,于是16位cpu的寻址方式是: 物理地址=段地址×16+偏移地址。这叫做实模式。但随着历史的演进,cpu已经支持32位(地址总线可以36位)、甚至64位(地址总线可以40位)。为了提升寻址能力和安全性,也为了兼容16位系统,在setup阶段就开始准备从16位实模式往32位保护模式做准备。
在32位保护模式下,段寄存器中存储是段选择子。根据段选择子的索引去查询全局描述符表以获得段描述符,其中存储着基地址,物理地址=基地址+偏移地址。
一旦开启了分段机制,还要多一步:
即程序员写代码时给出的地址叫逻辑地址,其中包含段选择子和偏移地址两部分。通过分段后得到的叫线性地址,线性地址通过分页后得到的叫物理地址。比如我们得到的线性地址是32位,分页机制大概是这样运作的:
线性地址被分为高 10 位、中间 10 位、后 12 位。高 10 位在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。这个过程是由硬件MMU(内存管理单元)完成的。
现在我们继续上一节,将head程序跳转至setup_paging开启分页:
setup_paging:mov ecx,1024*5 ;用来计数xor eax,eaxxor edi,edipushf ;保存所有标志, 这里主要为了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+8],pg2+7mov [eax+12],pg3+7mov edi,pg3+4092mov eax,00fff007h ;16Mb-4096+7 (r/w user,p)std
L3: stosd ;将eax的内容复制到edi,复制4字节,并将edi加/减4个字节sub eax,00001000hjge L3popfxor eax,eaxmov cr3,eaxmov eax,cr0or eax,80000000hmov cr0,eaxret
上述代码的意义就是在内存中一次写好页目录表和页表,之后开启cr0寄存器的分页开关。
首先按当前分页机制:
页目录项有10位,最多可以指示1K项的页表
页表项也有10位,最多可以指示1K项的页起始地址
线性地址低12位,最多可以指示4K偏移地址,4K为一个页。
分页后总共可访问的虚拟内存空间为:1024*1024*4K=4G。但当时Linux 0.11认为,总共可使用的内存不会超过16M。于是4(页表数)* 1024(页表项数) * 4KB(一页大小)= 16MB,即1个页目录表 + 4个页表就可搞定。这也就是ecx计数器设为1024*5的原因,意在通过rep stosd将5个页结构全部清0。
接着填写页目录中的项,共有4个页表只需设置4项(页目录和每个页表项本身也占据4K,每项4byte,一共可以设置1k个项/条目)。来看mov [eax],pg0+7
回顾下在实模式下,ds基地址,eax偏移地址所代表的内存单元:ds×16+eax
在保护模式下,ds段选择子,如0x10指向的是全局描述符表中的第二个段描述符(数据段描述符),里面内容中的段基址是 0。exa偏移地址所代表的内存单元:0+eax (这里+4/+8/+12是因为每一项4字节,32位)。
而页目录项结构与页表中项结构一样,"pg0+7"表示:0x00001007,是页目录表中的第1项。则
第1个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000
第1个页表的属性标志 = 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写。
于是我们用此方法把所需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了!