LOADING

加载过慢请开启缓存 浏览器默认开启

NEMU PA3 阶段二(实现分段机制)

2024/10/26

分段

前置知识在上一篇文章:IA-32/Linux内存管理

这里要开始看指导手册了。

首先在kernel/include/common.h 中定义宏 IA32_SEG, 然后重新编译 kernel

make run之后,si 1,发现第一个指令就执行不了了

查看指导手册

1.设置GDTR

2.将 CR0的PE位置1, 切换到保护模式

3.使用ljmp 设置CS寄存器

4.设置DS, ES, SS 寄存器

5.为 C代码设置堆栈

6.跳转到init()函数继续进行初始化工作

(1)

在 CPU_state 结构中添加GDTR, CR0和各种段寄存器, 包括CS, DS, ES, SS, 其具体结构请参考i386手册。80386中还引入了两个新的段寄存器GS 和FS, 不过我们不会用到它们, 因此可以不模拟它们的功能。LDT我们也不 会用到, 和LDT相关的内容也不必模拟。你还需要在restart()函数中对CR0 寄存器进行初始化, 让模拟的计算机在”开机”的时候运行在”实模式”下。

首先实现段寄存器

typedef struct{
    uint16_t selector;				// 段选择符
    uint32_t base, limit, type;		// 段描述符高速缓存 段描述符的信息
} S_reg;

说实话我也不知道为啥这么写

然后在cpu_state里面声明S_reg类型的变量

union{ // 段选择符
        struct{
            S_reg sreg[4];	// 为了方便swaddr_read和seg_translate
        };
        struct{
            S_reg CS, DS, SS, ES;	// 代码段,数据段,堆栈,扩展
        };
    };

注:union(联合体)在c/c++是一种数据结构,这里面的所有成员共享一块内存,内存的大小是最大成员的大小。在这里,union的成员是两个结构体,由于共享一块内存,cpu.sreg[0]就相当于cpu.CS的引用,cpu.serg[1]就相当于cpu.DS的引用,…

添加枚举类型enum { R_CS, R_DS, R_SS, R_ES};

实现CR0可以看libcommon/x86-inc/cpu.h里面,这里有CR0和CR3,我们不用自己写了。因此直接在NEMU2021/nemu/include/cpu/reg.h文件里面include这个头文件即可。

#include "../../lib-common/x86-inc/cpu.h"

然后在cpu_state里面声明cr0和cr3的变量即可。CR0 cr0; CR3 cr3;

还有GDTR寄存器(在cpu_state里面)

struct {
 uint32_t base, limit;	// GDT的 首地址 和 长度
} GDTR;

以及读取GDT的结构:

typedef struct{
    union{
        struct{
            uint16_t lim1, b1;
        };
        uint32_t p1;
    };
    union{
        struct{
            uint32_t b2: 8, a: 1, type: 3, s: 1, dpl: 2, p: 1, lim2: 4;
            uint32_t avl: 1, : 1,x: 1, g: 1,b3: 8;
        };
        uint32_t p2;
    };
}Sreg_info;	// 用于读取DGT的内容(相当于段寄存器的描述符cache)
Sreg_info sreg_info;

然后回忆以下restart()在monitor.c中,在这里面初始化CR0

一开始工作在实模式,且不启用分页

static void init_cr0(){
    cpu.cr0.protect_enable = 0; // 实模式
    cpu.cr0.paging = 0;			// 不分页
}

添加到restart即可。

(2)

添加lgdt指令。

查看i386手册330页

图中的LIDT为:加载中断描述符表,nemu中我们不会用到

/2表示opcode后面跟一个ModR/M字节,且上两位,中间三位,下三位里面的中间三位解释为opcode,为2

可能是16位,也可能是32位,所以我们直接用_v后缀读取操作数即可。

#ifndef __LGDT_H__
#define __LGDT_H__

make_helper(lgdt_rm_v);

#endif
#include "cpu/exec/helper.h"

#define DATA_BYTE 2
#include "lgdt-template.h"
#undef DATA_BYTE

#define DATA_BYTE 4
#include "lgdt-template.h"
#undef DATA_BYTE

make_helper_v(lgdt_rm)
#include "cpu/exec/template-start.h"

#define instr lgdt

static void do_execute() {
    /* 目标地址共有 6Bytes 的内容,存放limit和base */
    // 16位操作数:16bits limit + 24bits base | 2+3 Bytes
    // 32位操作数:16bits limit + 32bits base | 2+4 Bytes(最高字节存放 高位基址位)
    cpu.GDTR.limit = swaddr_read(op_src -> addr, 2, R_DS);
    if (op_src -> size == 2)
        cpu.GDTR.base = swaddr_read(op_src -> addr + 2, 3, R_DS);
    else{
        cpu.GDTR.base = swaddr_read(op_src -> addr + 2, 4, R_DS);
    }
    print_asm_template1();
}

make_instr_helper(rm);

#include "cpu/exec/template-end.h"

这是三个文件里面的代码,就是将那个地址里面的内容读到寄存器里面。地址里面的前两个字节表示限界,后3/4个字节表示基地址

别忘了#include "data-mov/lgdt.h"

以及将exec.c对应位置的指令修改一下。由于0F开头,在第二个组里面,由于是01,所以在group7,由于是/2,所以在group7里面的第三个位置。

(3)

添加opcode为0F 20和0F 22的mov指令, 使得我们可以设置/读出CR0。 设置CR0后, 如果发现CR0的PE位为1, 则进入IA-32保护模式, 从此所 有虚拟地址的访问(包括swaddr_read()和swaddr_write())都需要经过段级地址转换。

在对应的指令文件(mov-template)中添加:

#if DATA_BYTE == 4

make_helper(mov_cr2r) {
    uint8_t tmp = instr_fetch(eip+1, 1);
    uint8_t cr = (tmp >> 3) & 7;	// 倒数4~6位
    uint8_t reg = tmp & 7;			// 后3位
    if(cr == 0) {
        reg_l(reg) = cpu.cr0.val;
        print_asm("mov cr0 %%%s", REG_NAME(reg));
    }
    else if(cr == 3) {
        reg_l(reg) = cpu.cr3.val;
        print_asm("mov cr3 %%%s", REG_NAME(reg));
    }
    return 2;
}
make_helper(mov_r2cr) {
    uint8_t tmp = instr_fetch(eip+1, 1);
    uint8_t cr = (tmp >> 3) & 7;	// 倒数4~6位
    uint8_t reg = tmp & 7;			// 后3位
    if(cr == 0) {
        cpu.cr0.val = reg_l(reg);
        print_asm("mov %%%s cr0", REG_NAME(reg));
    }
    else if(cr == 3) {
        cpu.cr3.val = reg_l(reg);
        print_asm("mov %%%s cr3", REG_NAME(reg));
    }
    return 2;
}

#endif

以及在mov.h中添加。

(4)

为了实现段级地址转换, 你需要对swaddr_read()和swaddr_write()函数作 少量修改。其中 sreg 记录了当前段级地址转换所用到的段寄存器的编码, 关于段寄存器的 编码, 请查阅 i386 手册。你需要理解段级地址转过的过程, 然后实现 seg_translate()函数。再次提醒, 在 NEMU中, 只有进入保护模式之后才会进行 段级地址转换。

在include里面的memory.h里面先把函数声明修改了。

void swaddr_write(swaddr_t, size_t, uint32_t, uint8_t);以及 uint32_t swaddr_read(swaddr_t, size_t, uint8_t);

以及MEM宏

#define MEM_R(addr, sreg) swaddr_read(addr, DATA_BYTE, sreg)                // 添加了sreg
#define MEM_W(addr, data, sreg) swaddr_write(addr, DATA_BYTE, data, sreg)

在memory中修改swaddr_read以及swaddr_write

/*虚拟地址*/
/* 虚拟地址->线性地址 */
lnaddr_t seg_translate(swaddr_t addr, size_t len, uint8_t sreg) { 
    if (cpu.cr0.protect_enable == 0) return addr;
    return cpu.sreg[sreg].base + addr;
}
//读虚拟地址
uint32_t swaddr_read(swaddr_t addr, size_t len, uint8_t sreg) {
    assert(len == 1 || len == 2 || len == 4);
    lnaddr_t lnaddr = seg_translate(addr, len, sreg);
    return lnaddr_read(lnaddr, len);
}
//写虚拟地址
void swaddr_write(swaddr_t addr, size_t len, uint32_t data, uint8_t sreg) {
    assert(len == 1 || len == 2 || len == 4);
    lnaddr_t lnaddr = seg_translate(addr, len, sreg);
    lnaddr_write(lnaddr, len, data);
}

(5)

为了实现段寄存器的捆绑规则, 你还需要

1.在 Operand 结构体中添加成员sreg。

2.修改read_ModR_M()中的代码, 以确定是和DS, SS中的哪一个进行捆绑, 然后设置rm->sreg, 这样swaddr_read()和swaddr_write()就可以使用正确的 段寄存器了。

3.修改宏 MEM_W()和 MEM_R(), 以及所有调用 swaddr_read()和 swaddr_write()的代码, 为它们添加段寄存器的参数。

特别地:opcode为A0 , A1, A2, A3的mov指令使用DS寄存器。一些堆栈操作指令会隐式使用SS寄存器。instr_fetch()总是使用CS寄存器。在monitor中, x和p命令读出内存时, 使用DS寄存器;bt命令打 印栈帧链时, 使用SS寄存器。关于字符串操作指令使用的段寄存器, 请查阅i386手册。

在nemu/include/cpu/decode/operand.h里面修改结构体

struct {
    swaddr_t addr;
    uint8_t sreg;
};

nemu/src/cpu/decode/modrm.c里面设置rm->sreg

在readModRM里面:

if (rm->reg == R_EBP || rm->reg == R_ESP) { // 栈相关,用SS
    rm->sreg = R_SS;
}
else rm->sreg = R_DS;
rm->val = swaddr_read(rm->addr, rm->size, rm->sreg);	// swaddrr改了,这里要改

接下来,修改所有使用swaddr的地方即可。

(6)

添加opcode为8E的mov指令, 使得我们可以设置段寄存器。设置段寄存 器时, 还需要将段的一些属性读入到段寄存器的描述符cache部分(在i386 手册中被称为”隐藏部分”, invisible part), 我们只需要读入段的 base 和 limit 就可以了, 其它属性在NEMU中不使用.

另外还有两点需要注意:

GDTR 中存放的GDT首地址是线性地址。

IA-32 中规定不能使用mov指令设置CS寄存器, 但切换到保护模式之后, 下一条指令的取指就要用到CS寄存器了。解决这个问题的一种方式是在restart() 函数中对CS寄存器的描述符cache部分进行初始化, 将base初始化为0, limit 初始化为0xffffffff 即可

我们先来实现mov

make_helper(mov_sreg2rm) {
    uint8_t modrm = instr_fetch(eip + 1, 1);
    uint8_t sreg = (modrm >> 3) & 7;
    uint8_t reg = (modrm & 7);
    cpu.sreg[sreg].selector = reg_w(reg);
    sreg_set(sreg);									// 更新段描述符高速缓存
    print_asm("mov %s sreg%d", REG_NAME(reg), sreg);
    return 2;
}

以及在reg.c中的更新描述符cache(不知道啥是描述符cache,看IA-32/Linux内存管理)

void sreg_set(uint8_t id) {	// 根据段描述符 更新 段描述符高速缓存
    lnaddr_t chart_addr = cpu.GDTR.base + ((cpu.sreg[id].selector >> 3) << 3);	//段描述符地址
    sreg_info.p1 = lnaddr_read(chart_addr, 4);
    sreg_info.p2 = lnaddr_read(chart_addr + 4, 4);
    cpu.sreg[id].base = sreg_info.b1 + (sreg_info.b2 << 16) + (sreg_info.b3 << 24);
    cpu.sreg[id].limit = sreg_info.lim1 + (sreg_info.lim2 << 16) + (0xfff << 24);
    if (sreg_info.g == 1) {	//粒度位(G):0表示段界限单位是B;1表示4KB
        cpu.sreg[id].limit <<= 12;
    }
}

在monitor.c里面初始化CS

static void init_cs() {
    cpu.CS.base = 0, cpu.CS.limit = 0xffffffff;
}

(7)

为了设置CS寄存器, 你需要实现ljmp指令, 即JMP ptr16:32形式的jmp 指令, 其作用是”Jump intersegment, 6-byte immediate address”, 更多 信息请查阅i386手册。

#if DATA_BYTE == 4
make_helper(ljmp){
    cpu.eip = instr_fetch(cpu.eip + 1, 4, R_CS) - 7;                  // 后面eip会+7,所以先-7
    cpu.CS.selector = instr_fetch(cpu.eip + 1 + 4, 2, R_CS);          // 设置CS寄存器
    sreg_set(R_CS);                                             // 更新CS描述符高速缓存
    print_asm("ljmp 0x%x 0x%x",instr_fetch(cpu.eip + 1 + 4, 2, R_CS), instr_fetch(cpu.eip+1, 4), R_CS);
    return 7;
}
#endif

直接在jmp里面写即可。

至此,分段机制已经完成,阶段二结束。