分段
前置知识在上一篇文章: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里面写即可。
至此,分段机制已经完成,阶段二结束。