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 时期的寻址要求,对于读操作,我们在虚拟地址层有这样的修改:
| 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,储存了下列的信息:
| 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;
|
在理解了段表之后,我们继续理解段选择符的构成:
| 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?我们先取 selector 的 Index 部分,在 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() 中:
| #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
对应的函数也非常的显然
| // 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_l 和 mov_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.