Skip to content

PA3-2 note

TODO: 实现 NEMU 的 Protect Mode (partly)

Intro

先从内存层次的角度介绍从实模式到保护模式的演变

这里虚拟地址(virtual address, vaddr)和逻辑地址(logical address)等价

我们称目前实现的 NEMU 为早期的实模式,在实现不同层级的地址转换时采用非常直接的方式(以读操作为例):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint32_t paddr_read(paddr_t paddr, size_t len) 
{
    uint32_t ret = 0;
#ifdef CACHE_ENABLED
        ret = cache_read(paddr, len);
#else
        ret = hw_mem_read(paddr, len);
#endif
    return ret;
}

uint32_t laddr_read(laddr_t laddr, size_t len)
{
    return paddr_read(laddr, len);
}

uint32_t vaddr_read(vaddr_t vaddr, uint8_t sreg, size_t len)
{
    // sreg unused
    assert(len == 1 || len == 2 || len == 4);
    return laddr_read(vaddr, len);
}

不难发现,虚拟地址 vaddr -->线性地址 laddr --> 物理地址 paddr 在逐层转换时没有任何的实际转换行为,直接映射,可以理解为 vaddr == laddr == paddr

在最原始的 8086 设计中,寄存器只有 16 位,因此寻址时能够寻址的内存大小为 $2^{16} \text{ Byte} = 64 \text{ KB}$,为了扩展可寻址空间,我们引入若干段寄存器,不同的段寄存器绑定不同的场合:

偏移量(offset)类型 绑定的段寄存器
代码段(对应eip) CS
数据访问 DS
堆栈访问(对应esp和ebp) SS
特殊类型访问(如movs) ES

在引入了 sreg 之后,计算地址的方式变成了 “基址 + 偏移量”,其中vaddr 成为了偏移量 offset,而基址由 sreg 值直接决定:

physical address = (seg_reg << 4) + offset

此时物理地址的位数扩展到 20 位,寻址空间提升到 $2^{20} \text{ Byte} = 1 \text{ MB}$,满足了 8086 时期的寻址要求,对于读操作,我们在虚拟地址层有这样的修改:

1
2
3
4
5
6
7
8
9
uint32_t vaddr_read(vaddr_t vaddr, uint8_t sreg, size_t len)
{
    assert(len == 1 || len == 2 || len == 4);

    uint32_t seg_base = cpu.segReg[sreg].val << 4;  // cpu.sreg.val
    uint32_t physical_addr = seg_base + vaddr;

    return paddr_read(physical_addr, len);
}

从实模式切换到保护模式,我们有一个特殊的 0 号控制寄存器 CR0,其专门有一个 bit 位 PE 用于判断是否进入保护模式(PE = 1 时为保护模式)。现代的操作系统默认都是保护模式,在初始化完成后会将 PE 初始化为 1,进入保护模式

在保护模式下,寻址模式发生了很大的变化:程序层给出的逻辑地址不再是单纯的 32bit 地址,而是由 16bit 段选择符和 32bit 段内偏移量组成的内容:

logical address = [selector : offset](48bit)

linear address = base address + offset(32bit)

physical address = linear address (不考虑分页机制,其在 PA3-3 学习)

附:在保护模式下,sreg 的内容发生了改变,从单纯的存储基址变成了存储段选择符等一系列内容的寄存器,具体的划分你可以参考 “实现” 板块的结构体定义,其中 selector 是 sreg 的可见部分,程序可以直接访问

如何通过 selector 获得 base address?我们事先准备一个段表,其由一系列连续的段表项组成,每个段表项都是一个 64 bit 的段描述符,每个描述符描述一个段,其记录了每个端的首地址 base address,以及其他必要的信息(段的长度 limit 等等)

我们可以简单将段表理解为一个一位数组,每个元素存储一个 64bit 数,即段描述符。

段表的首地址存储在一个特殊寄存器 GDTR,储存了下列的信息:

1
2
3
4
typedef struct {
    uint16_t limit;  // GDT limit
    uint32_t base;   // GDT base address, which is linear addr
} GDTR;

指令集中,lgdt 指令负责从内存加载 GDTR 的内容

这里有一个段描述符的图示以及在 NEMU 中的模拟实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
COPIED FROM CH-MANUAL:

DESCRIPTORS USED FOR APPLICATIONS CODE AND DATA SEGMENTS

  31                23              15              7              0
 +-----------------+-+-+-+-+---------+-+-----+-+-----+-+-----------------+
 |               | | | |A|       | |     | |     | |                 |
 |   BASE 31..24   |G|X|O|V| LIMIT   |P| DPL |1| TYPE|A|  BASE 23..16   | 4
 |               | | | |L| 19..16  | |   | |     | |                 |
 |-----------------+-+-+-+-+---------+-+-----+-+-----+-+-----------------|
 |                                 |                                   |
 |      SEGMENT BASE 15..0       |     SEGMENT LIMIT 15..0       | 0
 |                                 |                                   |
 +-----------------------------------+-----------------------------------+

           A      - ACCESSED
           AVL  - AVAILABLE FOR USE BY SYSTEMS PROGRAMMERS
           DPL  - DESCRIPTOR PRIVILEGE LEVEL
           G      - GRANULARITY
           P      - SEGMENT PRESENT

-----------------------------------------------------------------------------
// From nemu/include/memory/mmu/segment.h
typedef union SegmentDescriptor {
    struct
    {
        uint32_t limit_15_0 : 16;
        uint32_t base_15_0 : 16;
        uint32_t base_23_16 : 8;
        uint32_t type : 4;
        uint32_t segment_type : 1;
        uint32_t privilege_level : 2;
        uint32_t present : 1;
        uint32_t limit_19_16 : 4;
        uint32_t soft_use : 1;
        uint32_t operation_size : 1;
        uint32_t pad0 : 1;
        uint32_t granularity : 1;
        uint32_t base_31_24 : 8;
    };
    uint32_t val[2];
} SegDesc;

在理解了段表之后,我们继续理解段选择符的构成:

1
2
3
4
5
6
7
8
9
15                      3 2   0
+-------------------------+-+---+
|                         |T|   |
|          INDEX          | |RPL|
|                         |I|   |
+-------------------------+-+---+

TI  - TABLE INDICATOR
RPL - REQUESTOR'S PRIVILEGE LEVEL

因为 NEMU 不实现特权等级相关的内容(RPL),并且只考虑全局段表 GDT 而不考虑局部段表 LDT(TI),所以我们只需要关注 Index 即可

(描述符表就是段表)

回到之前的问题:如何通过 selector 获得 base address?我们先取 selectorIndex 部分,在 GDT 中找到对应位置上的段描述符,读取段描述符的 base 部分作为 base address


实现

首先是器件实现,根据不同器件的内存划分(参见 i386 Manual),定义若干器件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
typedef struct {
    uint32_t limit :16;
    uint32_t base :32;
}GDTR;

typedef union {
    struct {
        uint32_t pe :1;
        uint32_t mp :1;
        uint32_t em :1;
        uint32_t ts :1;
        uint32_t et :1;
        uint32_t reserve :26;
        uint32_t pg :1;
    };
    uint32_t val;   
}CR0;

typedef struct {
    // the 16-bit visible part, i.e., the selector
    union {
        uint16_t val;
        struct {
            uint32_t rpl :2;
            uint32_t ti :1;
            uint32_t index :13;
        };
    };

    // the invisible part, i.e., cache part
    struct {
        uint32_t base;
        uint32_t limit;
        uint32_t type :5;
        uint32_t privilege_level :2;
        uint32_t soft_use :1;
    };
}SegReg;

// ...

GDTR gdtr; // GDTR
union { // segment registers
    SegReg segReg[6];
    struct { SegReg es, cs, ss, ds, fs, gs; };
};
CR0 cr0; // control register 0

器件的初始化已经给出了实现,在 init_cpu() 中:

1
2
3
4
5
6
7
8
#ifdef IA32_SEG
    cpu.cr0.val = 0x0;
    cpu.gdtr.base = cpu.gdtr.limit = 0x0;
    for (i = 0; i < 6; i++)
    {
        cpu.segReg[i].val = 0x0;
    }
#endif

接着是函数实现:

第一步:完成 segment_translate 函数,你已经知道了

linear address = base address + offset

对应的函数也非常的显然

1
2
3
4
5
// return the linear address from the virtual address and segment selector
uint32_t segment_translate(uint32_t offset, uint8_t sreg)
{
    return cpu.segReg[sreg].base + offset;
}

顺便在 memory.c 中对 vaddr 相关的函数进行修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
uint32_t vaddr_read(vaddr_t vaddr, uint8_t sreg, size_t len)
{
    assert(len == 1 || len == 2 || len == 4);
    if (cpu.cr0.pe) {
        laddr_t laddr = segment_translate(vaddr, sreg);
        return laddr_read(laddr, len);
    } else {
        return laddr_read(vaddr, len);
    }
}

void vaddr_write(vaddr_t vaddr, uint8_t sreg, size_t len, uint32_t data)
{
    assert(len == 1 || len == 2 || len == 4);
    if (cpu.cr0.pe) {
        laddr_t laddr = segment_translate(vaddr, sreg);
        laddr_write(laddr, len, data);
    } else {
        laddr_write(vaddr, len, data);
    }
}

第二步:实现段寄存器的重加载操作(load_sreg),也就是根据段寄存器的可见部分(selector)加载它的不可见部分(不可见部分程序无法访问,因此需要由 CPU 自行维护,从 GDT 中获取最新数据并更新)

这个函数是为了针对控制寄存器和段寄存器的特殊 mov 以及 ljmp 指令服务的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// load the invisible part of a segment register
void load_sreg(uint8_t sreg)
{
    uint32_t index = cpu.segReg[sreg].index;

    // 从 GDT 中读取描述符
    SegDesc desc;
    desc.val[0] = laddr_read(cpu.gdtr.base + (index << 3), 4);
    desc.val[1] = laddr_read(cpu.gdtr.base + (index << 3) + 4, 4);

    // 解析 base
    uint32_t base = (desc.base_15_0) | (desc.base_23_16 << 16) | (desc.base_31_24 << 24);

    // 解析 limit
    uint32_t limit = (desc.limit_15_0) | (desc.limit_19_16 << 16);

    // 写回不可见部分
    cpu.segReg[sreg].base = base;
    cpu.segReg[sreg].limit = limit;
    cpu.segReg[sreg].type = (desc.segment_type << 4) | desc.type;
    cpu.segReg[sreg].privilege_level = desc.privilege_level;
    cpu.segReg[sreg].soft_use = desc.soft_use;
}

// another way, pay attention to the extern use of hw_mem
void load_sreg(uint8_t sreg)
{
    SegReg *sReg = &cpu.segReg[sreg];

    assert(sReg->ti == 0);

    SegDesc* desc = (SegDesc *)((uint32_t) hw_mem + cpu.gdtr.base + 8 * cpu.segReg[sreg].index);

    uint32_t base = (desc->base_15_0) | (desc->base_23_16 << 16) | (desc->base_31_24 << 24);
    uint32_t limit = desc->limit_15_0 | (desc->limit_19_16 << 16);

    sReg->base = base;
    sReg->limit = limit;
    //...
}

第三步:完善涉及到保护模式操作的指令

lgdt 的全称是 Load Global Descriptor Table Register,用法是:lgdt [gdt_pointer]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
make_instr_func(lgdt) {
    OPERAND mem;

    int len = 1 + modrm_rm(eip + 1, &mem);

    // read limit first
    mem.data_size = 16;
    operand_read(&mem);
    cpu.gdtr.limit = mem.val;   // stored to gdtr.limit

    // then base
    mem.addr += 2;
    mem.data_size = 32;
    operand_read(&mem);
    cpu.gdtr.base = mem.val;    // stored to gdtr.base
    return len;
}

然后是 ljmp (长跳转)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
make_instr_func(jmp_far_imm)
{  
#ifdef IA32_SEG
    OPERAND opr;

    // offset
    opr.type = OPR_IMM;
    opr.data_size = data_size;
    opr.addr = eip + 1;
    operand_read(&opr);
    uint32_t upd_eip = opr.val;

    // sreg
    opr.sreg = SREG_CS;
    opr.data_size = 16;
    opr.addr += data_size / 8;
    operand_read(&opr);
    uint16_t upd_cs = opr.val;

    // jmp
    cpu.cs.val = upd_cs;
    load_sreg(SREG_CS); // notice here
    cpu.eip = upd_eip;
#endif
    return 0;
}

我们在实现保护模式的切换时用到了控制寄存器 CR0,因此和控制寄存器相关的 mov 函数 mov_c2r_l mov_r2c_lmov_rm2s_w 需要顺带实现

代码不给了,直接对着已经实现的 make_instr_func(mov_zrm82r_v) 改改就行了,区别不大


My problems during the debugging

lgdt 指令出现段错误先考虑 load_sreg 的实现

记得 mov_c2r_l mov_r2c_l mov_rm2s_w需要完成,否则会 You have used reference implementation, DO NOT submit this version.