这一篇,是重点!我们将去讲解操作系统根据代码(逻辑)地址去访问真实物理地址的全过程。
将把全面几节的东西全部用上,并完全梳理,完善细节。
前面讲了分段、分页机制,他们都可以实现,从虚拟地址(地址空间)向物理地址的转换。但是,实际使用过程中,使用的是分段+分页机制,段页结合。
段页结合
全过程分析(高能)
我们现在采用边实验边讲解翻译全过程。
写了一段 c 代码,编译,然后在 Linux 0.11 中,进行调试
#include <stdio.h>
int i = 0x12345678;
int main(void)
{
printf("The logical/virtual address of i is 0x%08x", &i);
fflush(stdout);
while (i)
;
return 0;
}
注意:我们程序中的变量 i 的大小为 0x12345678。
我们想做的是,通过编译,找到变量 i 的逻辑地址,然后经过一系列的地址转换,获得物理地址。通过查看物理地址的内容,是否是 0x12345678。
获得虚拟地址
将运行的代码进行反编译,可以看到 cmp dword ptr 这一部分。这一部分,对应的就是上面c语言的 while(i) 部分。
可以看到熟悉的 ds:0x3004
,这是什么?
这就是我们之前分段章节里面的间接寻址。也就是说,我们要找到 ds 段的基址,然后加上3004的偏移量。
这里的 ds:0x3004 就是这一部分。你会发现 0x3004 只有16位啊,下图的偏移量标记的是32位。
因为在 Linux 0.11中,给每个进程划分了 64M 的虚拟内存,2的16次方就是64M。
下图中的偏移量位32位,是给每个进程划分了 4G 的虚拟内存。
注意:看下图红色方框部分,其中的0-15位选择符用来选择程序中的段的。后面的0-31偏移值,是每个段中的偏移量。
分段机制,假设一个程序中有很多个段(个数由选择符的位数决定),而且每个段都可以占有一个大小的空间(由偏移值位数决定)。
在下图中,由于选择符0-15中只有14位用来指定段的,所以下图中的虚拟地址,可以指定214个段,每个段可以有4G(232)的大小空间。
虚拟地址解读
从上面,我们获得变量 i 的虚拟地址为 ds: 0x3004。
通过下图,我们查看寄存器,可以获得ds=0x0017,所以ds:0x3004=0x0017: 3004。
我们来看ds=0x0017的解读。
这其实也叫选择符,看下图。
重点看,TI 位,也就是2号位。0x0017=0x 17 = 0x 0001 0111,也就是 TI 位为1。
当 TI 为0时,说明我们要找的有关段表信息就在 GDT表中,我们可以通过继续对 0x0017的3-15位进行解读,获取有关段表信息在 GDT表中的索引。
当 TI 为1的时候,说明我们要找的 有关段表信息 在 LDT表中。
段描述符(段表的相关信息)
段描述符
每个段都有一个段描述符。
段描述符指定段的大小、访问权限和段的特权级、段类型以及段的第一字节在线性地址空间中的位置(也就是段基址)。
GDT表
GDT表,是全局描述表。从这里的 描述 二字与上面的 段描述符可以看出:GDT表中保存着上面提到的段描述符。
LDT表
LDT表,是局部描述表。里面也保存着段描述符。
GDTR寄存器
此寄存器,记录着 GDT表的基址。
LDTR寄存器
跟我们之前说的选择符是一样的,它表明了 LDT表在 GDT表中的位置。
我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。
LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同,LDTR的内容是一个段选择子。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。
注意,LDT表中也保存着描述符,是我们需要的。
也就是说,我们首先要获取 LDT表的描述符,然后在 LDT表中获取我们需要的段描述符。
获得LDT段描述符
我们已经知道,我们的段选择符为ldtr。
所以,我们现在得获得 GDTR 和 LDTR寄存器中的内容。
可以看到,LDTR寄存器中的值为 0x0068, GDTR寄存器中的值为 0x00005cb8。
所以,我们将0x0068=0000 0000 0110 1000,我们保留3-15位,1101=13。
所以我们现在知道了,我们需要的段描述符在GDT表开始位置的第13个位置处。
我们在GDT表获得偏移13个位置处的内容。
解读段描述符
我们已经获得了段描述符的内容了,离目标越来越近了。只要解读出段描述符的内容,我们就可以获得段表的基址了。
其解读如下,我们利用上面的结果,并结合下图,去获得基址。
所以,我们获得 LDT表的物理地址为0x00fd52d0。
获取我们所需段表的描述符
就像我们之前谈到的,LDT表存储的也是段描述符。
所以我们也需要像之前那样,去获取相应位置的段描述符,然后进行解读。
还记得我们之前的ds=0x0017嘛?
0x0017=0x 17 = 0x 0001 0111,其中索引为2。
现在我们获得 LDT表基址开始处的内容。
因为索引都是从0开始的,所以获取的段描述符为 0x00003ffff 0x10c0f300。
解读方式如上,这样我们求得段表的基址为0x10000000。
之后,将段基址与偏移量相加,即可获得线性地址。0x10003004。
线性地址解读
因为采用了多级页表,所以分页页目录和页表。其中位数解读,如图所示。
注意,页目录的基址存储在 cr3 寄存器中。
如下图,我们获得的页目录表的基址为0x0。
说明页目录表的基址为 0。
因为0x10003004=0x 0001 0000 0000 0000 0011 0000 0000 0100。
所以知道,目录为 0001 0000 00 ,为64。
页面为 00 0000 0011,为3。
偏移为0000 0000 0100,为4。
获取页目录项
我们要获得页目录号为64的内容:
解读页表项
可以看到基址为12到31为,所以地址为0x00fa5000。
获取页表项
页表所在物理页框为0x00fa5000位置,从该位置开始查找3号页表项,得到:
这个解读同上,所以最后获得的基址为0x00f99000。
加上前面提到的偏移4,最终的物理地址为:0x00f99004。
验证
最后,我们查看这个物理地址的内容,发现,是我们程序中设置的i的值。