Lab 2 - Exercise
https://www.cnblogs.com/bdhmwz/p/4960034.html
https://www.cnblogs.com/oasisyang/p/15495908.html
https://zhuanlan.zhihu.com/p/188757141
https://blog.csdn.net/fang92/article/details/47322241
https://blog.csdn.net/a747979985/article/details/95371949
https://blog.csdn.net/m0_54695206/article/details/121711177
https://zhuanlan.zhihu.com/p/165104094
https://github.com/peanutzhen/MIT6.828-2017lab/blob/master/kern/pmap.c
背景知识
通过实验1,我们知道系统的启动流程:
-
系统启动自动执行BIOS,BIOS初始化设备。
-
BIOS将磁盘的首512个字节(也就是第一个扇区)读取到内存0x7c00处(第一个扇区存放的是boot.S和main.C),并将系统控制权交给boot.S和main.C
-
boot.S将CPU工作模式从实模式切换到保护模式,关闭了系统中断,并且在最后调用main.C中的bootmain函数
-
main.C为了将内核的ELF文件读入内存,首先将磁盘首4096个字节(也就是第一页)读取到内存0x10000处(暂存地址),然后判断ELF文件是否一个合法的ELF文件,然后根据PROGRAM HEADER TABLE将指定的 segments 加载到内存指定的位置中去(也就是加载内核,内核物理地址首部在0x100000,入口在0x10000c),最后跳转0x10000c进入内核。
-
进入内核之后,首先执行的是entry.S
entry.S主要干了这么几件事儿:为了使内核自身能够在页机制下运行,将标号entry_pgdir指向的页目录(entrypgdir.C)的物理地址传送给cr3,并修改cr0启用页机制(在开启页机制前不可直接使用线性地址,因为高址区域还没有被映射);通过一条间接转移(地址存在寄存器中)指令使得EIP指向高址端,从而内核真正实现在高址端运行;设置EBP为0、ESP指向标号bootstacktop处,调用i386_init函数。至此,entry的任务就完成了。
entry.S的核心就是设置CR3和CR0,通过设置CR3来使用手动创建的两级页表,通过设置CR0开启分页模式,使得内核能够在页机制下正常运行。
这个手动创建的二级页表在 entrypgdir.C 中定义
在之前,其实已经设置了一次CR0了(打开保护模式),但是并没有打开分页,那么当时bootmain是怎样被正确链接的,C代码为什么可以执行?其实是设置了GDT,把虚拟地址(线性地址)直接变成物理地址(没有任何转化) -
i386_init
在i386_init中,先将内核ELF文件中的BSS section在内存中清空为0,这是完成加载ELF格式文件的最后一步,且被清空的区域位于内核的末端;接着调用cons_init函数初始化控制设备,主要和显示相关,该函数执行完毕后才可以正常调用cprintf将信息输出到控制台,我们不必知道细节;接下来调用mem_init初始化内存管理;最后是monitor函数,它循环地接受和执行用户输入的命令,即扮演shell的角色,但和xv6不同的是,这里的monitor并非是一个用户进程,此时JOS还没有创建任何进程。
mem_init函数就是我们要在lab2中完善的对象。在entry.S中,JOS创建了一个使得内核能够在页机制下正常运行的两级页表
(这些内存空间足以帮助内核最开始的C语言程序(需要使用虚拟地址)完成真正内核页表的构建,例如kern_pgdir)。但我们的目标远不止于此,我们还要建立更多的映射关系,满足JOS作为一个多任务系统上程序间隔离、保护的需要。在这里,mem_init仅创建内核的地址空间,因为我们还没有任何用户程序。具体地,mem_init需要创建一个页目录,并从该页目录出发建立一些映射关系——而我们要做的,就是实现建立这些映射关系所需要的函数工具,并通过这些工具真正创建好需要的映射。
事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常
Exercise 1
在文件kern/pmap.c中,必须实现以下函数的代码(可能按照给定的顺序)。
boot_alloc()
mem_init() (only up to the call to check_page_free_list(1)) //设置一个两级page table
page_init() //初始化 page structure 和 memory free list
page_alloc() //分配物理页
page_free() //释放物理页
check_page_free_list() and check_page_alloc() test your physical page allocator. You should boot JOS and see whether check_page_alloc() reports success. Fix your code so that it passes. You may find it helpful to add your own assert()s to verify that your assumptions are correct.
按照题目要求我们要补充以上几个中的代码,我们要首先理解这几个函数的的作用。
在这之前我们要首先了解当前内存的结构
目前内存的结构如图所示:

然后看到 mem_init() 函数,他是这几个函数中的主函数,理解这个函数是重点。
代码太长了,我把代码放在另一篇文章 https://www.cnblogs.com/pilBolog/p/15770114.html
接下来可以对照着看
mem_init() 函数首先执行了i386_detect_memory()查明了机器有多大内存。
然后执行
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
PGSIZE定义在文件中:4096
第一行代码的作用是在内核代码的结束位置end之后分配一个PGSIZE(单位是kb)大小的空间,并返回所分配空间的首地址给kern_pgdir,kern_pgdir就是页目录。
第二行的代码作用是将刚刚申请的内存空间(kern_pgdir~kern_pgdir+PGSIZE)里的值全部变为0,即内存初始化。
在这里,boot_alloc(PGSIZE)的作用是检验end(上图中)后面是否有PGSIZE大小的空闲空间,如果有则返回end地址。
kern_pgdir接收这个地址并作为自己的起始地址。
然后执行 kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
这句没弄懂,先不看。
然后执行
pages = boot_alloc(npages* sizeof(struct PageInfo));
memset(pages,0,npages*sizeof(struct PageInfo));
pages定义在文件开头:struct PageInfo pages; 代表页表
PageInfo结构定义在kern/pmap.h; 代表页的结构
npages定义在首先执行的i386_detect_memory()函数中,代表全部内存页数
他们的具体定义请看 pmap.c 中的注释
这两行代码跟上面的基本一样,也是起到分配空间的作用,这里主要是给 pages 页表分配空间。
在这里,boot_alloc(PGSIZE)的作用是检验 kern_pgdir 后面是否有 npages sizeof(struct PageInfo) 大小的空闲空间,如果有则返回kern_pgdir末尾地址。
pages接收这个地址并作为自己的起始地址。
执行以上代码之后,物理内存分布如图:

Pages是一个存储 PageInfo结构 的数组。pages[0]映射物理内存04k,pages[1]映射物理内存4k8k,如此类推。
主要通过 pmap.h 中的 page2pa 和 pa2page 实现 pages[0] 到物理地址,物理地址到 pages[0] 之间的转换

此图来自:https://blog.csdn.net/a747979985/article/details/95371949
然后执行了page_init();
page_init()函数中确定了哪几页可以分配,哪几页不可分配,并且将可以分配的空闲页面存储在了page_free_list中,如下图。
红色的部分不可分配,蓝色的部分可以分配。

在这个实验系统中,采用了一个比较巧妙的结构,通过page_free_list这个变量来完成页面的申请和调用。

在https://blog.csdn.net/fang92/article/details/47322241中有详细的讲解

然后执行
check_page_free_list(1); //检查page_free_list上的页面是否合理。
check_page_alloc(); //检查物理页面分配器(page_alloc()、page_free()和page_init())。
check_page(); //check page_insert, page_remove, &c
check_kern_pgdir(); //检查初始页目录是否已正确设置
lcr3(PADDR(kern_pgdir)); //将目前正在使用的页目录(在entry.S中设置的)切换为kern_pgdir(在mem_init中设置的)
check_page_free_list(0); //检查page_free_list上的页面是否合理。
//在entry.S中我们已经设置了cr0以开启分页,这里是配置我们关心的其他标志
cr0 = rcr0();
cr0 |= CR0_PE|CR0_PG|CR0_AM|CR0_WP|CR0_NE|CR0_MP;
cr0 &= ~(CR0_TS|CR0_EM);
lcr0(cr0);
check_page_installed_pgdir(); //使用已安装的kern_pgdir检查page_insert, page_remove, &c
这段代码做了一系列的检查,并且设置了cr0的其他标志位。mem_init 函数到此结束。
我们已经通过mem_init(),boot_alloc(),page_init()这三个函数在内核代码之后(也就是end之后)新的创建了页目录,页表。并且生成了空闲页面链表。接下来还有两个函数没有完成,他们分别是page_alloc() 和 page_free(),这两个函数时执行时jos使用的是新的页目录页表。
首先来看 page_alloc(),通过注释我们知道这个函数用于分配物理页。
那么怎么分配呢?
我们首先通过空闲页面链表找到空闲的页面,并将其从空闲页面链表移除,
然后我们使用 page2kva 函数获取该页对应的物理地址对应的虚拟地址,并根据虚拟地址进行内存初始化。
page2kva获取物理地址原理:参数是一个空闲页 pp,假设pp是第二页,第二页即*pages[1],其起始地址是pages[1],
我们可以用 pages[1] 减去 pages 首地址,结果为 1(在c中,这里的1是指一个字节),再左移12位,得到0x00001000 这就是空闲页第一页对应的物理地址。
注意
pages[1]和pages[1]的区别,
sizeof(pages[1])=4个字节,
sizeof(pages[1])=sizeof(struct PageInfo),每个pageInfo里面有两个变量,一个位指针,一个为int, 即pageInfo的大小为8字节。
同时
在c中,pages[1] = pages+1,pages[2]=pages+2,这里的+1,+2其实是指在原地址基础上增加1,2个字节。
同时了解:物理地址为虚拟地址减去kernbase
举个例子:虚拟地址是0xf0001000, 对应的物理地址为 0x00001000。kernbase为0xF0000000。
最后还有 page_free() 把这个完成我们的Exercise 1就结束了。
page_free(struct PageInfo *pp)的作用是给定一个页面,page_free()将这个页面重新加入到空闲页面链表中。
以上几个函数的实现都在 https://www.cnblogs.com/pilBolog/p/15770114.html 中。
Exercise 2
阅读 Intel 80386 Reference Manual 的五六章了解803806的内存管理和保护模式。
Virtual, Linear, and Physical Addresses
In x86 terminology, a virtual address consists of a segment selector and an offset within the segment!
在x86中,
逻辑地址由段选择子和段内偏移组成
线性地址(虚拟地址)是逻辑地址进行段翻译后得到的
物理地址是逻辑地址经过段翻译和页翻译后得到的

虚拟地址也称作LOGICAL ADDRESS

机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。
Page Translation
线性地址的结构以及转换为物理地址过程:

虚拟地址,也就是线性地址,被拆成了三部分,都是一种索引index,分别索引的是Page Directory, Page Table, Page Frame。从page directory中读出page table 的地址,在从读到的page table地址中读到page frame的地址,索引page frame之后,就得到相应物理地址上的内容。
页目录和页表的大小都是 4kb。
对于开发者来说,page directory, page table都是两个数组,拿到page directory的头部指针,和虚拟地址一起,就可以确定物理地址。
每个域对应长度
线性地址,也就是虚拟地址,的格式如下:

每个域包含bit的个数,也就是长度,决定了每个域对应的数组的长度。我们可以很方便地得到每个域对应的长度:
page_len = 2 ** 12 = 4096 // OFFSET
page_table_len = 2 ** 10 = 1024 // PAGE
page_dir_len = 2 ** 10 = 1024 // DIR
这些长度应该这样看。一个page directory指向1024个page directory entry,一个page directory entry指向了1024个page table,一个page table entry指向了1024个page frame,一个page frame中包含4096Bytes。所以32位的虚拟地址最多可以映射
102410244096 = 4GB 的物理地址。
Entry格式

page directory和page table结构相同,都是Entry的格式。
page directory 管理 page table。
page table 管理 page frame。
可查看Exercise 3 中的图
对于page directory来说,entry中12-31位上的PAGE FRAME ADDRESS就是一个page table的基地址。对于page table来说,这个地址是一个page frame的基地址。通过一个虚拟地址,获得3个索引,一次访问这3个结构,就可以得到物理地址了。
总结分析
已经知道页目录和页表的大小都是4k,且页目录项和页表项结构一样,都是Entry结构。
一个Entry结构大小是32位,即4个字节。所以页目录和页表都有1024个项。(刚好对应虚拟地址的2231,1221位)。
Exercise 3
背景知识
A C pointer is the "offset" component of the virtual address.
In boot/boot.S, we installed a Global Descriptor Table (GDT) that effectively disabled(有效地极禁用了) segment translation by setting all segment base addresses to 0 and limits to 0xffffffff.
Hence(因此) the "selector" has no effect and the linear address always equals the offset of the virtual address.(因为段翻译被仅用了,所以linear address = virtual address)
In lab 3, we'll have to interact(交互) a little more with segmentation to set up privilege levels(特权等级), but as for memory translation, we can ignore segmentation throughout(全部,整个) the JOS labs and focus solely(仅仅) on page translation.
Recall(回想) that in part 3 of lab 1, we installed a simple page table(页表) so that the kernel could run at its link address of 0xf0100000, even though it is actually loaded in physical memory just above the ROM BIOS at 0x00100000.
This page table mapped only 4MB of memory. In the virtual address space layout you are going to set up for JOS in this lab, we'll expand this to map the first 256MB of physical memory starting at virtual address 0xf0000000 and to map a number of other regions of the virtual address space.
在 boot/boot.S 我们设置了一个GDT,变相的禁用了段翻译,使得虚拟地址(LOGICAL ADDRESS)经过转换后仍然等于线性地址。
在lab 1 part 3 中,我们设置了一个简单的 page table, 将 将范围从0xf0000000到0xf0400000的线性地址地址转换为物理地址0x00000000到0x00400000,通过这样,kernel 可以运行在 0xf0100000 的线性地址上,实际运行在 0x00100000 的物理地址上。
练习内容
While GDB can only access QEMU's memory by virtual address, it's often useful to be able to inspect(检查) physical memory while setting up virtual memory.
Review the QEMU monitor commands from The Lab Tools Guide, especially the xp command, which lets you inspect physical memory.
To access the QEMU monitor, press Ctrl-a c in the terminal (the same binding returns to the serial(串行) console).
Use the xp command in the QEMU monitor and the x command in GDB to inspect memory at corresponding(对应的) physical and virtual addresses and make sure you see the same data.
Our patched(打补丁的) version of QEMU provides an info pg command that may also prove useful: it shows a compact(紧凑的) but detailed representation(表示) of the current page tables, including all mapped memory ranges(范围), permissions, and flags.
QEMU also provides an info mem command that shows an overview of which ranges of virtual addresses are mapped and with what permissions.
qemu 的xp 命令解释
xp/Nx paddr
Display a hex dump of N words starting at physical address paddr. If N is omitted, it defaults to 1. This is the physical memory analogue of GDB's x command.
显示从物理地址paddr开始的N个单词的十六进制值,N不输入的时候默认为1。
GDB的 x 命令解释
x/ 10x 十六进制显示
-
窗口1:在lab文件夹下运行sudo make qemu-nox-gdb 启动后,按下 Ctrl-a c 进入命令行模式。
-
窗口2:运行sudo make gdb。
-
在窗口2中,使用b *0x10000c 打断点,使用 c 命令运行到 物理地址0x10000c,再连续使用 si 一直运行到保护模式和分页开启(与lab1 Exercise7类似)
-
在窗口1中,运行命令 xp/10x 0x100000,
-
在窗口2中,运行命令 x/10x 0xf0100000
![image]()
可以看到 物理地址 0x100000 和虚拟地址 0xf0100000 存储的值一样。 -
在窗口1运行 info pg 命令 和 info mem 命令
![image]()
PDE 是页目录 PTE是页表
[000]是第一个页目录的入口地址,这个页目录包含三个页表,他们的范围分别是[00000-000ff]、[00100-00100]、[00101-003ff]。
info mem 命令之后显示的的是虚拟地址和物理地址的对应关系和权限。
图中 虚拟地址 0000000000000000-0000000000400000 和 00000000f0000000-00000000f0400000 都被 映射到物理地址:
0000000000000000-0000000000400000,对于用户程序来讲,前者权限是可读,后者权限是可读可写。
Exercise 4
继续实现 kern/pmap.c中的几个函数:
- pgdir_walk()
- boot_map_region()
- page_lookup()
- page_remove()
- page_insert()
从mem_init()调用的Check_page()进行测试。在继续之前,您应该确保它报告成功。
页目录存放多个页表的入口地址,如pgdir[1]代表第一个页表的入口地址,pgdir[2]代表第二个页表的入口地址。
页表存放多个页的入口地址,设pgtab = pgdir[1],那么pgtab[1],pgtab[2]分别代表第一个页表第一个页,第一个页表第二个页。
boot_map_region() 调用 pgdir_walk
pgdir_walk() 给定*pgdir,va
根据给出的虚拟地址va获取页目录索引 pd_index 和页表索引 pt_index,从而获得页帧的入口地址。如果pgdir[pd_index](页表入口地址)存在,则直接返回pgdir[pd_index]pt_index,如果pgdir[pd_index]不存在,则创建一个,然后返回 pgtab[pt_index]。
主要作用是根据va创建/获得页帧(页表项)。
boot_map_region() 给定*pgdir,va,size,pa
将虚拟地址 va~va+size 映射到物理地址 pa~pa+size
根据size批量调用pgdir_walk()获取 size/4096 个页帧入口地址
再将物理地址分页地存储到页帧
主要作用是根据va获得页帧(pgdir_walk),再将pa(物理地址)和页帧建立联系。
page_lookup()
看懂这张图,就能大概明白pgdir_walk中的代码:




浙公网安备 33010602011771号