实验6-内存映射和共享
实验内容请查看实验指导手册
绪论
老规矩,我们来梳理一下,这个实验做了什么事情:
-
地址翻译:
- 在Bochs中运行一个
test.c
文件,先是通过相应的命令语句,查看到了程序中i
的逻辑地址为ds:0x3004
; - 通过寄存器
ldtr
和gdtr
我们获取到了LDT
表的内容,通过ds
段寄存器存放的段选择子0x0017
,我们在LDT
表的对应位置找到了ds
段的段描述符为“0x00003fff 0x10c0f300”
; - 有了
ds
段描述符,我们通过组合得到段ds
的线性地址为0x10003004
。通过查页表,最终我们成功找到了变量i
的物理地址:0x00fa6004
。
地址翻译的过程如下图所示:
- 在Bochs中运行一个
-
共享内存的生产者/消费者程序:
- 编写一个内核程序
shm.c
,自己实现两个函数shmget(), shmat()
。在shmget()
函数中,调用get_free_page()
找到一块空闲的物理内存;在shmat()
函数中,调用put_page()
函数将物理内存的物理地址转换为线性地址。
- 编写一个内核程序
一、实验内容
1.跟踪地址翻译过程
(1)准备
在Ubuntu的终端中通过sudo ./mount-hdc
命令挂载虚拟机,在(hdc/usr/root)目录下新建test.c
,代码如下:
#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;
}
使用命令sudo umount hdc
退出挂载,再在终端中依次输入./dbg-asm
, c
启动Bochs
在Bochs中编译并运行test.c
,得到以下结果:
只要test.c
不变,0x00003004
这个值在任何人的机器上都是一样的。即使在同一个机器上多次运行test.c
,也是一样的。
test.c
是一个死循环,只会不停占用CPU,不会退出。
(2)暂停
当test.c
运行的时候,在命令行窗口按“ctrl+c”,Bochs会暂停运行,进入调试状态。绝大多数情况下都会停在test内,显示类似如下信息:
(0) [0x00fc8031] 000f:00000031 (unk. ctxt): cmp dword ptr ds:0x3004, 0x00000000 ; 833d0430000000
其中加粗的“000f”如果是“0008”,则说明中断在了内核里。那么就要c,然后再ctrl+c,直到变为“000f”为止。
如果显示的下一条指令不是“cmp …”,就用“n”命令单步运行几步,直到停在“cmp …”,如下图所示:
使用命令“u /7”,显示从当前位置开始7条指令的反汇编代码,如下:
这就是test.c
中从while开始一直到return的汇编代码。变量i
保存在ds:0x3004
这个地址(这个地址就是上课时学到的虚拟地址)并不停地和0进行比较,直到它为0,才会跳出循环。
现在,开始寻找ds:0x3004
对应的物理地址。
(3)段表
ds:0x3004
是虚拟地址,ds这两个字母表明这个虚拟地址属于ds
段。
要将虚拟地址翻译成物理地址,首先要找到段表,然后通过ds
的值在段表中找到ds
段的具体信息,才能继续进行地址翻译。每个在IA-32上运行的应用程序都有一个段表,叫LDT
,段的信息叫段描述符。
LDT
在哪里呢?ldtr
寄存器是线索的起点。为了能让CPU定位LDT
表,设置了ldtr
寄存器。通过它可以在GDT
(全局描述符表)中找到LDT
的物理地址。
用“sreg”命令:
可以看到ldtr
的值是0x0068=0000000001101000
(二进制),表示LDT
表存放在GDT
表的1101
(二进制)=13
(十进制)号位置(每位数据的意义参考后文叙述的段选择子)。而GDT
的位置已经由gdtr
明确给出:在物理地址的0x00005cb8
处。
用“xp /32w 0x00005cb8”查看GDT
表的前16项,如下:
GDT
表中的每一项占64位(8个字节),要查找的是GDT
表的13号位置,该处的地址应该是0x00005cb8 + 13*8
。
使用命令:xp /2w 0x00005cb8 + 13*8
这里我们得到了两个地址:0x52d00068
, 0x000082fd
。将这两个地址,同之前“sreg”命令得到的ldtr
行中的dl
和dh
相比较,如果对得上,那么说明我们的操作没有问题,可以继续后面的步骤
“0x52d00068 0x000082fd” 将其中的加粗数字组合为“0x00fd52d0”,这就是 LDT 表的物理地址(为什么这么组合,参考后文介绍的段描述符)。
输入命令 xp /8w 0x00fd52d0,得到:
这就是LDT
表的前4项内容了。后面我们将会用到LDT
表中的内容。
(4)段描述符
在保护模式下,段寄存器有另一个名字,叫段选择子,因为它保存的信息是一个段描述符表(Segment Descriptor Table)中某一描述符项在表中的索引值。用这个索引值可以从段描述符表中“选择”出相应的段描述符。
先看看ds
选择子的内容,还是用“sreg”命令:
可以看到,ds
的值是0x0017
。段选择子是一个16位寄存器,它各位的含义如下图:
RPL是请求特权级,当访问一个段时,处理器要检查RPL和CPL(放在cs的位0和位1中,用来表示当前代码的特权级),即使程序有足够的特权级(CPL)来访问一个段,但如果RPL(如放在ds中,表示请求数据段)的特权级不足,则仍然不能访问,即如果RPL的数值大于CPL(数值越大,权限越小),则用RPL的值覆盖CPL的值。
.
TI是表指示标记,如果TI=0,则表示段描述符(段的详细信息)在GDT(全局描述符表)中,即去GDT中去查;而TI=1,则去LDT(局部描述符表)中去查。
而我们查到的ds=0x0017=0000000000010111
(二进制)。也即RPL=11(二进制)
,可见是在最低的特权级(因为在应用程序中执行)。
TI=1
,表示查找LDT
表,索引值为10(二进制)= 2(十进制),表示找LDT
表中的第3个段描述符(从0开始编号)。
LDT
和GDT
的结构一样,每项占8个字节(64位)。所以在上面查到的LDT
表中第3项“0x00003fff 0x10c0f300”
就是搜寻好久的ds
的段描述符了。
用“sreg”输出中ds
所在行的dl
和dh
值可以验证找到的描述符是否正确。
接下来看看段描述符里面放置的是什么内容:
可以看到,段描述符是一个64位二进制的数,存放了3块基地址和段限长等重要的数据。
位P(Present)是段是否存在的标记;
位S用来表示是系统段描述符(S=0)还是代码或数据段描述符(S=1);
四位TYPE用来表示段的类型,如数据段、代码段、可读、可写等;
DPL是段的权限,和CPL、RPL对应使用;
位G是粒度,G=0表示段限长以位为单位,G=1表示段限长以4KB为单位;
其他内容就不详细解释了。
段描述符组合成线性地址,就是将段描述符中的基地址由低到高组合在一起。
(5)线性地址
费了很大的劲,我们终于是找到了虚拟地址ds:0x3004
所对应的段描述符。实际上我们需要的只有段基址一项数据,即段描述符“0x00003fff 0x10c0f300” 中加粗部分组合成的 0x10000000
。这就是ds
段在线性地址空间中的起始地址。用同样的方法也可以算算其它段的基址,都是这个数。
段基址+段内偏移,就是线性地址了。所以ds:0x3004
的线性地址就是:
0x10000000 + 0x3004 = 0x10003004
用“calc ds:0x3004”命令可以验证这个结果。
发现是对的,当前已经找到了该进程在虚拟内存中的线性地址了,下一步就是找物理地址。
从线性地址计算物理地址,需要查找页表。线性地址变成物理地址的过程如下:
首先需要算出线性地址中的页目录号、页表号和页内偏移,它们分别对应了32位线性地址的10位+10位+12位,所以0x10003004
的页目录号是64,页号3,页内偏移是4。(不信你们可以自己转换成2进制算一下)
IA-32下,页目录表的位置由CR3
寄存器指引。“creg”命令可以看到:
CR3 = 0x00000000
说明页目录表的基址为0。看看其内容,“xp /68w 0”:
页目录表和页表中的内容很简单,是1024个4字节。这4字节(32位)中前20位是物理页框号,后面是一些属性信息(其中最重要的是最后一位P)。
我们需要翻译的线性地址是0x10003004
的页目录号是64,用(“xp /w 0+64*4”)查看:
页目录项的物理页框号为0x00fa9
,即页表在物理内存0x00fa9000
位置为起点的一块内存中。
从0x00fa9000
位置开始查找3号页表项,得到(xp /w 0x00fa9000+3*4):
得到0x00fa6067
,其中0x00fa6
便是页表项的物理页框号。
(7)物理地址
最终结果马上就要出现了!
线性地址0x10003004
对应页表项的物理页框号为0x00fa6
,和页内偏移0x004
接到一起,得到0x00fa6004
,这就是变量i
的物理地址。可以通过两种方法验证。
第一种方法是用命令“page 0x10003004”,可以得到信息:“linear page 0x10003000 maps to physical page 0x00fa6000”。
第二种方法是用命令“xp /w 0x00fa7004”,可以看到:
这个数值确实是test.c
中i
的初值。
现在,通过直接修改内存来改变i
的值为0,命令是:setpmem 0x00fa7004 4 0
,表示从0x00fa7004
地址开始的4
个字节都设为0
。然后再用“c”命令继续Bochs的运行,可以看到test.c
退出了,说明i
的修改成功了,此项实验结束。
这一部分的实验,u1s1不算很难,刚开始做或者看肯定是有很多不明所以的地方。个人建议做实验的同时,二刷一次李老师的视频,并结合《Linux0.11完全注释》这本书的5.3节,相信你会有很大的收获。
2.基于共享内存的生产者-消费者程序
这个实验要求我们在Linux0.11上,让生产者和消费者共用一块物理内存(实验5是使用文件读写)。这里就涉及到所学的内存管理相关知识。
Linux0.11上是没有shmget(), shmat()
函数的,需要自己编写系统调用实现。
不将生产者和消费者放置同一个文件pc.c
,而是生产者是producer.c
,消费者是consumer.c
,两个程序都是单进程的。
通过地址跟踪实验,我们一步步的从逻辑地址——》得到虚拟地址——》找到线性地址——》最后找到物理地址。
现在呢,我们要让两个进程共享一个物理内存,完成实验的思路也即:先开辟一块物理内存——》然后完成物理地址和线性地址之间的映射——》再找到对应的虚拟地址。
1.获得空闲物理页面
实验报告当中也给出了方法:通过调用get_free_page()
函数,该函数会帮我们找到内存当中空闲区域并返回该区域起始物理地址。这一步我们在shmget
函数中实现:
shmget()函数代码
2.地址映射
(1)
有了空闲的物理页面,接下来需要完成线性地址和物理页面的映射。使用put_page(addr1, addr2)
建立物理地址addr1和线性地址addr2的映射。
这部分的代码,我们放在函数shmat()
中执行:
shmat()代码
3.寻找空闲的虚拟地址空间
有了空闲物理页面,也有了建立线性地址和物理页面的映射,但要完成本实验还需要能获得一段空闲的虚拟地址空闲。
实验结果如下图所示,不知道为什么做不对。生产者生产完10个资源后,消费者不能去消费?
二、回答问题
- 对于地址映射实验部分,列出你认为最重要的那几步(不超过 4 步),并给出你获得的实验数据。
(1)获得程序中变量
i
的逻辑地址:ds:3004
(2)查LDT表得到段描述符:“0x00003fff 0x10c0f300”
(3)通过段描述符得到线性地址:0x10003004
(4)查页表得到物理地址:0x00fa6004
- test.c 退出后,如果马上再运行一次,并再进行地址跟踪,你发现有哪些异同?为什么?
逻辑地址和虚拟地址不变,页目录地址是操作系统放置的, 物理分页变了,所以物理地址变了。原因是每次进程加载后都有64M的虚拟地址空间,导致段基址不同。
而数据段偏移量不变,这是编译时就设置完毕的。
参考资料:
1.哈工大操作系统实验—lab6:地址映射与共享
2.蓝桥云课-操作系统原理与实践
3.虚拟地址、逻辑地址、线性地址、物理地址
4.Linux-0.11操作系统实验6-地址映射与共享