uCore lab

发布于 2019-12-27  218 次阅读


Lab1

练习1 理解通过make生成执行文件的过程

问题1:操作系统镜像文件ucore.img是如何一步一步生成的?

预DEBUG

执行make观察编译过程:

出现报错,定位到报错代码:tools/sign.c

bootblock.out文件大小为600B,超出了一个扇区512B的限制。

所以跟进到boot的源代码,将其修改以减小目标文件的大小:

修改bootmain.c中的全局变量为宏定义:

make成功且bootblock.out成功减小到了488B

观察make的输出

# 构建bin/kernel
+ cc kern/init/init.c
+ cc kern/libs/readline.c
+ cc kern/libs/stdio.c
+ cc kern/debug/kdebug.c
+ cc kern/debug/kmonitor.c
+ cc kern/debug/panic.c
+ cc kern/driver/clock.c
+ cc kern/driver/console.c
+ cc kern/driver/intr.c
+ cc kern/driver/picirq.c
+ cc kern/trap/trap.c
+ cc kern/trap/trapentry.S
+ cc kern/trap/vectors.S
+ cc kern/mm/pmm.c
+ cc libs/printfmt.c
+ cc libs/string.c
+ ld bin/kernel
# 构建sign工具与bin/bootblock
+ cc boot/bootasm.S
+ cc boot/bootmain.c
+ cc tools/sign.c
# 使用gcc编译器由tools/sign.c生成可执行文件bin/sign
    gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
    gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
# 使用ld命令链接/boot/bootasm.o、obj/boot/bootmain.o到obj/bootblock.o
+ ld bin/bootblock
    ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o 
    obj/boot/bootmain.o -o obj/bootblock.o
    'obj/bootblock.out' size: 472 bytes
    build 512 bytes boot sector: 'bin/bootblock' success!
# 构建ucore.img
dd if=/dev/zero of=bin/ucore.img count=10000 # 使用dd工具创建一个bin/ucore.img空文件
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.0456474 s, 112 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc # 使用dd工具将文件bin/bootblock写入bin/ucore.img, 参数conv=notrunc表示不截断输出文件
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.00281044 s, 182 kB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc # 使用dd工具将文件bin/kernel写入bin/ucore.img起始的1个block后,即bootblock后, 参数seek=1表示从输出文件开头跳过1个block开始写入
138+1 records in
138+1 records out
70775 bytes (71 kB) copied, 0.000473867 s, 149 MB/s

由此结合源码,分析整体项目架构:

  • 编译了部分C代码,生成uCore内核ELF文件kernel
  • 编译bootasm.S与bootmain.c生成bootloader的目标文件bootblock.out
  • 编译sign.c生成规范化工具,并用其将bootblock填充为以0x55 0xAA结尾的512字节的块文件
  • 由bootloader的ELF文件规范化为512B后的bootblock,与内核ELF文件kernel,合并得到可引导的磁盘映像ucore.img

练习2:使用qemu执行并调试lab1中的软件

任务:

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
  4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

qemu+GDB断点调试

由于我的实验环境并非使用了老师所提供的ubuntu磁盘镜像,而是物理机的Arch Linux,故直接使用result中的为原ubuntu定制的脚本会因为环境不同而出现各种问题。所以在此手动配置调试。

首先阅读Makefile中相应行,了解老师的代码究竟执行了什么操作:

lab1-mon: $(UCOREIMG)
                $(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -monitor stdio -hda $< -serial null" -g -monitor stdio -hda $< -serial null"
                $(V)sleep 2
                $(V)$(TERMINAL) -e "gdb -q -x tools/lab1init"

可以看到make lab1-mon主要完成了如下操作:

  • 第一个是让 qemu 把它执行的指令给记录下来,放到 q.log 这个地方
  • 第二个是和 gdb 结合来调试正在执行的 Bootloader。其中,gdb启动后执行的命令从tools/lab1init中读取

tools/lab1init:

file /bin/kernel
target remote :1234
set architecture i8086
b *0x7c00
continue
x /2i $pc

故操作如下:

shell中:

qemu-system-i386 -s ./bin/ucore.img

GDB中(使用了增强插件gef):

file ./bin/kernel

target remote :1234

b *0x7c00

c

成功断在了bootloader的起始代码处:

使用CGDB下断点于bootmain()处:

最后,下断点于BIOS的起始指令处:

此处需要qemu的-S参数以使得虚拟机启动即暂停,以断在BIOS起始代码处

动态调试中的指令与源码中的指令的比较

最开始gdb未设置set architecture i8086,以至于反汇编得到的代码是以i386的形式呈现的:

可以看到主要的区别为:

  • 源代码是以AT&T格式的汇编编写,而GDB默认将机器码反编译为intel格式的汇编代码
  • 寄存器被错误的反汇编呈现为了32位宽的寄存器。GDB中执行set architecture i8086后得以修复

练习3:分析bootloader进入保护模式的过程

bootloader能够开启i386的保护模式,使得程序进入了一个32位的寻址空间,使得寻址方式发生了变化。其需要完成如下事情:

seta20.1:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.1
​
    movb $0xd1, %al                                 # 0xd1 -> port 0x64
    outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port
​
seta20.2:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.2
​
    movb $0xdf, %al                                 # 0xdf -> port 0x60
    outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
  • 开启A20
    • i8086时代,CPU的数据总线是16bit,地址总线是20bit(20根地址总线),寄存器是16bit,因此CPU只能访问1MB以内的空间。因为数据总线和寄存器只有16bit,如果需要获取20bit的数据, 我们需要做一些额外的操作,比如移位。实际上,CPU是通过对segment(每个segment大小恒定为64K) 进行移位后和offset一起组成了一个20bit的地址,这个地址就是实模式下访问内存的地址:address = segment << 4 | offset理论上,20bit的地址可以访问1MB的内存空间(0x00000 - (2^20 - 1 = 0xFFFFF))。但在实模式下, 这20bit的地址理论上能访问从0x00000 - (0xFFFF0 + 0xFFFF = 0x10FFEF)的内存空间。也就是说,理论上我们可以访问超过1MB的内存空间,但越过0xFFFFF后,地址又会回到0x00000。上面这个特征在i8086中是没有任何问题的(因为它最多只能访问1MB的内存空间),但到了i80286/i80386后,CPU有了更宽的地址总线,数据总线和寄存器后,这就会出现一个问题: 在实模式下, 我们可以访问超过1MB的空间,但我们只希望访问 1MB 以内的内存空间。为了解决这个问题, CPU中添加了一个可控制A20地址线的模块,通过这个模块,我们在实模式下将第20bit的地址线限制为0,这样CPU就不能访问超过1MB的空间了。进入保护模式后,我们再通过这个模块解除对A20地址线的限制,这样我们就能访问超过1MB的内存空间了。注:事实上,A20就是第21根线,用来控制是否允许对 0x10FFEF 以上的实际内存寻址。称为A20 Gate默认情况下,A20地址线是关闭的(20bit以上的地址线限制为0),因此在进入保护模式(需要访问超过1MB的内存空间)前,我们需要开启A20地址线(20bit以上的地址线可为0或者1)。实现代码:bootasm.S中
  • 初始化GDT表(全局描述符表)
    • 在Protected Mode下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段积存器装入这个段描述符。但 Intel 为了保持向后兼容,将段积存器仍然规定为16-bit(尽管每个段积存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段积存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段积存器来直接引用64-bit的段描述符。怎么办?解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以 Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过 LGDT 指令将 GDT 的入口地址装入此积存器,从此以后,CPU 就根据此积存器中的内容作为 GDT 的入口来访问GDT了。GDT是Protected Mode所必须的数据结构,也是唯一的——不应该,也不可能有多个。另外,正如它的名字(Global Descriptor Table)所蕴含的,它是全局可见的,对任何一个任务而言都是这样。已解决的迷惑:Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址了。这样的情况下Linux只用到了GDT,不论是用户任务还是内核任务,都没有用到LDT。对于GDT表的设置:

asm.h:

/* Normal segment */
#define SEG_NULLASM                                             \
    .word 0, 0;                                                 \
    .byte 0, 0, 0, 0
​
#define SEG_ASM(type,base,lim)                                  \
    .word (((lim) >> 12) & 0xffff), ((base) & 0xffff);          \
    .byte (((base) >> 16) & 0xff), (0x90 | (type)),             \
        (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)

bootasm.S:

# Bootstrap GDT
.p2align 2                                          # force 4 byte alignment
gdt:
    SEG_NULLASM                                     # null seg
    SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel
    SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel
​
gdtdesc:
    .word 0x17                                      # sizeof(gdt) - 1
    .long gdt                                       # address gdt
    
lgdt gdtdesc
  • 使能和进入保护模式

使能:开启A20,初始化gdt后,将控制寄存器CR0PE(bit0)置为1即可:    

movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0

进入保护模式:

  • bootloader开始运行在实模式,物理地址为0x7c00,且是16位模式
  • bootloader关闭所有中断,方向标志位复位,ds,es,ss段寄存器清零
  • 打开A20使之能够使用高位地址线
  • 由实模式进入保护模式,使用lgdt指令把GDT描述符表的大小和起始地址存入gdt寄存器,修改寄存器CR0的最低位(orl $CR0PEON, %eax)完成从实模式到保护模式的转换,使用ljmp指令跳转到32位指令模式
  • 进入保护模式后,设置ds,es,fs,gs,ss段寄存器,堆栈指针,便可以进入c程序bootmain    
# Jump to next instruction, but in 32-bit code segment.
    # Switches processor into 32-bit mode.
    ljmp $PROT_MODE_CSEG, $protcseg
​
.code32                                             # Assemble for 32-bit mode
protcseg:
    # Set up the protected-mode data segment registers
    movw $PROT_MODE_DSEG, %ax                       # Our data segment selector
    movw %ax, %ds                                   # -> DS: Data Segment
    movw %ax, %es                                   # -> ES: Extra Segment
    movw %ax, %fs                                   # -> FS
    movw %ax, %gs                                   # -> GS
    movw %ax, %ss                                   # -> SS: Stack Segment
​
    # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
    movl $0x0, %ebp
    movl $start, %esp
    call bootmain

练习4:分析bootloader加载ELF格式的OS的过程

到了这一步,bootasm.S中的汇编代码便完成了它的全部任务,通过call bootmain指令将控制权控制权交给了bootmain.c中的C语言代码。

bootmain.c中的C语言代码的主要任务是将uCoreOS的内核ELF文件载入内存中,步骤如下:

bootloader如何读取硬盘扇区的

  • 由readsect()函数实现
  • bootloader进入保护模式并载入c程序bootmain
  • bootmain中readsect函数完成读取磁盘扇区的工作,函数传入一个指针和一个uint_32类型secno,函数将secno对应的扇区内容拷贝至指针处
  • 调用waitdisk函数等待地址0x1F7中低8、7位变为0,1,准备好磁盘
  • 向0x1F2输出1,表示读1个扇区,0x1F3输出secno低8位,0x1F4输出secno的8-15位,0x1F5输出secno的16-23位,0x1F6输出0xe+secno的24-27位,第四位0表示主盘,第六位1表示LBA模式,0x1F7输出0x20
  • 调用waitdisk函数等待磁盘准备好
  • 调用insl函数把磁盘扇区数据读到指定内存

bootloader是如何加载ELF格式的OS

  • 由bootmain()函数实现
  • 调用readseg函数从kernel头读取8个扇区得到elfher
  • 判断elfher的成员变量magic是否等于ELF_MAGIC,不等则进入bad死循环
  • 相等表明是符合格式的ELF文件,循环调用readseg函数加载每一个程序段
  • 调用elfher的入口指针进入OS

练习5:实现函数调用堆栈跟踪函数

首先研究一下堆栈跟踪函数的调用位置。可以发现是kern_init()函数(内核入口点)中的grade_backtrace()函数最终调用了print_stackframe(),而这个函数正是需要我们完成的。其最终目的是打印ebp、eip与当前指令执行处对应的参数(假定为4个,遵循了C标准调用的栈帧结构,32位下用栈传参)

void print_stackframe(void){  
    uint32_t ebp = read_ebp(), eip = read_eip();
    for (int i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
        // ebp向上移动1个字长为eip
        uint32_t *args = (uint32_t *)ebp + 2;
        // 向上每1个字长都为输入的参数
        for (int j = 0; j < 4; j ++)    cprintf("0x%08x ", args[j]);
        cprintf("\n");
        print_debuginfo(eip - 1);
        // ebp指针指向的位置向上一个地址为上一个函数的eip
        eip = ((uint32_t *)ebp)[1];
        // ebp指针指向的位置存储的上一个ebp的地址
        ebp = ((uint32_t *)ebp)[0];
    }
}

执行效果:

练习6:完善中断初始化和处理

请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

SETGATE宏的本质是设置生成一个4字节的中断描述表项:

#define SETGATE(gate, istrap, sel, off, dpl) {            \
    (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;        \
    (gate).gd_ss = (sel);                                \
    (gate).gd_args = 0;                                    \
    (gate).gd_rsv1 = 0;                                    \
    (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \
    (gate).gd_s = 0;                                    \
    (gate).gd_dpl = (dpl);                                \
    (gate).gd_p = 1;                                    \
    (gate).gd_off_31_16 = (uint32_t)(off) >> 16;        \
}

vector.S中保存了中断号定义,可以看到所有的中断服务例程最终都是跳到__alltraps进行处理的

最终idt_init()实现如下:

void idt_init(void) {
    extern uintptr_t __vectors[];
    for (int i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    }
    // 此处需要单独设置user to kernel的中断向量表项,因为DPL即访问权限不同
    SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
    lidt(&idt_pd);
}

请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”

依然先分析调用流程。trap.c中,trap()函数将一个中断帧传递给trap_dispatch()函数,然后使用一个switch结构进行传入中断号对应的操作。那么,我们所需要完成的代码便是在时钟中断对应的case中填入实现题目所述功能的代码。具体实现如下:

switch (tf->tf_trapno) {
    case IRQ_OFFSET + IRQ_TIMER:
        ticks ++;
        if (ticks % TICK_NUM == 0)    print_ticks();
        break;

Lab2

练习1:实现 first-fit 连续物理内存分配算法

该算法从空闲分区链首开始查找,直至找到一个能满足其大小要求的空闲分区为止。然后再按照作业的大小,从该分区中划出一块内存分配给请求者,余下的空闲分区仍留在空闲分区链中。

default_pmm.c中创建了一个pmm_manager结构体,用于管理物理内存:

const struct pmm_manager default_pmm_manager = {
    .name = "default_pmm_manager",
    .init = default_init,
    .init_memmap = default_init_memmap,
    .alloc_pages = default_alloc_pages,
    .free_pages = default_free_pages,
    .nr_free_pages = default_nr_free_pages,
    .check = default_check,
};

而我们需要完成其中的四个函数指针对应的函数实现:

  • default_init
    • 初始化空闲页块链表
  • default_init_memmap
    • 初始化每个空闲页
  • default_alloc_pages
    • 遍历空闲页块的链表,分配满足大小n的块
  • default_free_pages
    • 标记释放的页为空,将相应页块添加到空闲页块表

相关数据结构:

物理页的属性结构:

struct Page {
    int ref;                       
    uint32_t flags;                
    unsigned int property;       
    list_entry_t page_link;        
};

结构体成员变量意义如下:

  • ref : 表示这样页被页表的引用记数,这里应该就是映射此物理页的虚拟页个数。一旦某页表中有一个页表项设置了虚拟页到这个 Page 管理的物理页的映射关系,就会把 Pageref 加一。反之,若是解除,那就减一。
  • flags : 表示此物理页的状态标记,有两个标志位,第一个表示是否被保留,如果被保留了则设为 1(比如内核代码占用的空间)。第二个表示此页是否是 free 的。如果设置为 1 ,表示这页是 free 的,可以被分配;如果设置为 0 ,表示这页已经被分配出去了,不能被再二次分配。
  • property : 用来记录某连续内存空闲块的大小,这里需要注意的是用到此成员变量的这个 Page 一定是连续内存块的开始地址(第一页的地址)。
  • page_link : 是便于把多个连续内存空闲块链接在一起的双向链表指针,连续内存空闲块利用这个页的成员变量 page_link 来链接比它地址小和大的其他连续内存空闲块。

管理空闲页块的双向链表:

typedef struct {
    list_entry_t free_list;       
    unsigned int nr_free;          
} free_area_t;
​
free_area_t free_area;
​
#define free_list (free_area.free_list)
#define nr_free (free_area.nr_free)

default_init():

static void default_init(void) {
    list_init(&free_list); // 初始化记录空闲页块的链表
    nr_free = 0; // 初始状态空闲页数量为0
}

default_init_memmap():

static void default_init_memmap(struct Page *base, size_t n) { // 初始化以base为起始地址的n个page结构
    assert(n > 0); // 断言n>0
    struct Page *p = base;
    for (; p != base + n; p ++) {
        assert(PageReserved(p)); // 确认本页是否为保留页
        p->flags = p->property = 0; // 设置标志位
        set_page_ref(p, 0); // 清空引用
    }
    base->property = n; // 连续内存空闲块的大小为n,头一个空闲页块要设置数量
    SetPageProperty(base);
    nr_free += n;  // 空闲页数目 + n
    list_add_before(&free_list, &(p->page_link));//插入空闲页块的链表里面
}

default_alloc_memmap():

static struct Page * default_alloc_pages(size_t n) {
    assert(n > 0);
    if (n > nr_free) { // 所有的空闲页的加起来的大小也不够
        return NULL;
    }
    struct Page *page = NULL;
    list_entry_t *le = &free_list; // 从空闲块链表的头指针开始
    
    // 查找 n 个或以上空闲页块 若找到则判断是否大过 n 若是则将其拆分 并将拆分后的剩下的空闲页块加回到链表中
    while ((le = list_next(le)) != &free_list) {//依次往下寻找直到回到头指针处,即已经遍历一次
        // 此处 le2page 就是将 le 的地址 - page_link 在 Page 的偏移 从而找到 Page 的地址
        struct Page *p = le2page(le, page_link); // 由page_link的地址获取相应Page结构体的起始地址
        if (p->property >= n) { // 由于是first-fit,则遇到的第一个大于n的块就选中即可
            page = p;
            break;
        }
    }
    
    if (page != NULL) { // 成功找到满足要求的page
        if (page->property > n) {
            struct Page *p = page + n;
            p->property = page->property - n; // 如果选中的第一个连续的块大于n,只取其中的大小为n的块
            SetPageProperty(p);
            list_add(&(page->page_link), &(p->page_link)); // 将多出来的插入到 被分配掉的页块后面
        }
        list_del(&(page->page_link)); // 最后在空闲页链表中删除掉原来的空闲页
        nr_free -= n; // 当前空闲页的数目减n
        ClearPageProperty(page);
    }
    return page;
}

default_free_pages():

static void default_free_pages(struct Page *base, size_t n) {
    assert(n > 0);
    struct Page *p = base;
    for (; p != base + n; p ++) {
        assert(!PageReserved(p) && !PageProperty(p));
        p->flags = 0; // 修改标志位
        set_page_ref(p, 0);
    }
    base->property = n; // 设置连续大小为n
    SetPageProperty(base);
    list_entry_t *le = list_next(&free_list);
    
    // 合并到合适的页块中
    while (le != &free_list) {
        p = le2page(le, page_link); // 获取链表节点对应的Page起始地址
        le = list_next(le);
        if (base + base->property == p) {
            base->property += p->property;
            ClearPageProperty(p);
            list_del(&(p->page_link));
        }
        else if (p + p->property == base) {
            p->property += base->property;
            ClearPageProperty(base);
            base = p;
            list_del(&(p->page_link));
        }
    }
    
    nr_free += n;
    le = list_next(&free_list);
    
    // 将合并好的合适的页块添加回空闲页块链表
    while (le != &free_list) {
        p = le2page(le, page_link);
        if (base + base->property <= p) {
            break;
        }
        le = list_next(le);
    }
    list_add_before(le, &(base->page_link)); // 将每一空闲块对应的链表插入空闲链表中
}

练习2:实现寻找虚拟地址对应的页表项

这里要求我们完成pmm.c中的get_pte()函数。实现由虚地址找到相应的页表项地址的功能。

相关数据结构:

pde_t :

  • page directory entry 页目录表如上图
  • P (Present) 位:表示该页保存在物理内存中。
  • R (Read/Write) 位:表示该页可读可写。
  • U (User) 位:表示该页可以被任何权限用户访问。
  • W (Write Through) 位:表示 CPU 可以直写回内存。
  • D (Cache Disable) 位:表示不需要被 CPU 缓存。
  • A (Access) 位:表示该页被写过。
  • S (Size) 位:表示一个页 4MB 。
  • 9-11 位保留给 OS 使用。
  • 12-31 位指明 PTE 基质地址。

pte_t :

  • page table entry 页表如上图
  • P (Present) 位:表示该页保存在物理内存中。
  • R (Read/Write) 位:表示该页可读可写。
  • U (User) 位:表示该页可以被任何权限用户访问。
  • C (Cache Disable) 位:同 PDE D 位。
  • A (Access) 位:同 PDE 。
  • D (Dirty) 位:表示该页被写过。
  • G (Global) 位:表示在 CR3 寄存器更新时无需刷新 TLB 中关于该页的地址。
  • 9-11 位保留给 OS 使用。
  • 12-31 位指明物理页基址。

具体实现:

pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create) {
    pde_t *pdep = &pgdir[PDX(la)]; // 找到 PDE 这里的 pgdir 可以看做是页目录表的基址
    if (!(*pdep & PTE_P)) {         // 判断 PDE 指向的页表是否存在
        struct Page* page = alloc_page(); // 不存在就申请一页物理页
        if (!create || page == NULL) { // 不存在且不需要创建,返回NULL
            return NULL;
        }
        set_page_ref(page, 1); //设置此页被引用一次
        uintptr_t pa = page2pa(page);//得到 page 管理的那一页的物理地址
        memset(KADDR(pa), 0, PGSIZE); // 将这一页清空 此时将线性地址转换为内核虚拟地址
        *pdep = pa | PTE_U | PTE_W | PTE_P; // 设置 PDE 权限
    }
    return &((pte_t *)KADDR(PDE_ADDR(*pdep)))[PTX(la)];
}

练习3:释放某虚地址所在的页并取消对应二级页表项的映射

这里要求我们完成pmm.c中的page_remove_pte()函数

相关存储区:

TLB: translation lookaside buffer,是一块cache, 缓存PDE, PTE的内容,使得寻址并不都需要访问两次Memory(PDE,PTE)

具体实现:

static inline void page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
    if ((*ptep & PTE_P)) { //判断页表中该表项是否存在
        struct Page *page = pte2page(*ptep);// 将页表项转换为页数据结构
        if (page_ref_dec(page) == 0) { // 判断是否只被引用了一次,若引用计数减一后为0,则释放该物理页
            free_page(page);
        }
        *ptep = 0; // //如果被多次引用,则不能释放此页,只用释放二级页表的表项
        tlb_invalidate(pgdir, la); // 刷新 tlb
    }
}