mit 6.828 lab 代码和笔记,以及中文注释源代码已放置在github中:
https://github.com/yunwei37/xv6-labs
Part 3: The Kernel 内核
使用虚拟内存解决位置依赖性
内核的链接地址(由objdump打印)与加载地址之间存在(相当大的)差异;操作系统内核通常喜欢被链接并在很高的虚拟地址(例如0xf0100000)上运行,以便将处理器虚拟地址空间的下部留给用户程序使用。
- 链接地址 f0100000
- 加载地址 00100000
许多机器在地址0xf0100000上没有任何物理内存,因此我们不能指望能够在其中存储内核;将使用处理器的内存管理硬件将虚拟地址0xf0100000(内核代码期望在其上运行的链接地址)映射到物理地址0x00100000(引导加载程序将内核加载到物理内存中)。
这样,尽管内核的虚拟地址足够高,可以为用户进程留出足够的地址空间,但是它将被加载到PC RAM中1MB点的BIOS ROM上方的物理内存中。
在这个阶段中,仅映射前4MB的物理内存;
映射:kern/entrypgdir.c 中手写,静态初始化的页面目录和页面表。
直到kern / entry.S设置了CR0_PG标志,内存引用才被视为物理地址。
-
将范围从0xf0000000到0xf0400000的虚拟地址转换为物理地址0x00000000到0x00400000
-
将虚拟地址0x00000000到0x00400000转换为物理地址0x00000000到0x00400000
-
kern/entrypgdir.c:
#include <inc/mmu.h>
#include <inc/memlayout.h>
pte_t entry_pgtable[NPTENTRIES];
// entry.S页面目录从虚拟地址KERNBASE开始映射前4MB的物理内存
// (也就是说,它映射虚拟地址
// 地址[KERNBASE,KERNBASE + 4MB)到物理地址[0,4MB)
// 我们选择4MB,因为这就是我们可以在一页的空间中映射的表
// 这足以使我们完成启动的早期阶段。我们也映射
// 虚拟地址[0,4MB)到物理地址[0,4MB)这个
// 区域对于entry.S中的一些指令至关重要,然后我们
// 不再使用它。
//
// 页面目录(和页面表)必须从页面边界开始,
// 因此是“ __aligned__”属性。 另外,由于限制
// 与链接和静态初始化程序有关, 我们在这里使用“ x + PTE_P”
// 而不是更标准的“ x | PTE_P”。 其他地方
// 您应该使用“ |”组合标志。
__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
// 将VA的[0,4MB)映射到PA的[0,4MB)
[0]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
// 将VA的[KERNBASE,KERNBASE + 4MB)映射到PA的[0,4MB)
[KERNBASE>>PDXSHIFT]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};
// 页表的条目0映射到物理页0,条目1映射到
// 物理页面1,依此类推
__attribute__((__aligned__(PGSIZE)))
pte_t entry_pgtable[NPTENTRIES] = {
0x000000 | PTE_P | PTE_W,
0x001000 | PTE_P | PTE_W,
0x002000 | PTE_P | PTE_W,
0x003000 | PTE_P | PTE_W,
0x004000 | PTE_P | PTE_W,
0x005000 | PTE_P | PTE_W,
................
- kern/entry.S
/* See COPYRIGHT for copyright information. */
#include <inc/mmu.h>
#include <inc/memlayout.h>
# 逻辑右移
#define SRL(val, shamt) (((val) >> (shamt)) & ~(-1 << (32 - (shamt))))
###################################################################
# 内核(此代码)链接到地址〜(KERNBASE + 1 Meg),
# 但引导加载程序会将其加载到地址〜1 Meg。
#
# RELOC(x)将符号x从其链接地址映射到其在
# 物理内存中的实际位置(其加载地址)。
###################################################################
#define RELOC(x) ((x) - KERNBASE)
#define MULTIBOOT_HEADER_MAGIC (0x1BADB002)
#define MULTIBOOT_HEADER_FLAGS (0)
#define CHECKSUM (-(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS))
###################################################################
# 进入点
###################################################################
.text
# Multiboot标头
.align 4
.long MULTIBOOT_HEADER_MAGIC
.long MULTIBOOT_HEADER_FLAGS
.long CHECKSUM
# '_start'指定ELF入口点。 既然当引导程序进入此代码时我们还没设置
# 虚拟内存,我们需要
# bootloader跳到入口点的*物理*地址。
.globl _start
_start = RELOC(entry)
.globl entry
entry:
movw $0x1234,0x472 # 热启动
# 我们尚未设置虚拟内存, 因此我们从
# 引导加载程序加载内核的物理地址为:1MB
# (加上几个字节)处开始运行. 但是,C代码被链接为在
# KERNBASE+1MB 的位置运行。 我们建立了一个简单的页面目录,
# 将虚拟地址[KERNBASE,KERNBASE + 4MB)转换为
# 物理地址[0,4MB)。 这4MB区域
# 直到我们在实验2 mem_init中设置真实页面表为止
# 是足够的。
# 将entry_pgdir的物理地址加载到cr3中。 entry_pgdir
# 在entrypgdir.c中定义。
movl $(RELOC(entry_pgdir)), %eax
movl %eax, %cr3
# 打开分页功能。
movl %cr0, %eax
orl $(CR0_PE|CR0_PG|CR0_WP), %eax
movl %eax, %cr0
# 现在启用了分页,但是我们仍在低EIP上运行
# (为什么这样可以?) 进入之前先跳到上方c代码中的
# KERNBASE
mov $relocated, %eax
jmp *%eax
relocated:
# 清除帧指针寄存器(EBP)
# 这样,一旦我们调试C代码,
# 堆栈回溯将正确终止。
movl $0x0,%ebp # 空帧指针
# 设置堆栈指针
movl $(bootstacktop),%esp
# 现在转到C代码
call i386_init
# 代码永远不会到这里,但如果到了,那就让它循环死机吧。
spin: jmp spin
.data
###################################################################
# 启动堆栈
###################################################################
.p2align PGSHIFT # 页面对齐
.globl bootstack
bootstack:
.space KSTKSIZE
.globl bootstacktop
bootstacktop:
不在这两个范围之一内的任何虚拟地址都将导致硬件异常:导致QEMU转储计算机状态并退出。
练习7:
使用QEMU和GDB跟踪到JOS内核并在movl %eax, %cr0处停止。检查0x00100000和0xf0100000的内存。现在,使用stepiGDB命令单步执行该指令。同样,检查内存为0x00100000和0xf0100000。
在movl %eax, %cr0处停止:
(gdb) x 0x00100000
0x100000: add 0x1bad(%eax),%dh
(gdb) x 0xf0100000
0xf0100000 <_start-268435468>: add %al,(%eax)
si:
0x00100028 in ?? ()
(gdb) x 0x00100000
0x100000: add 0x1bad(%eax),%dh
(gdb) x 0xf0100000
0xf0100000 <_start-268435468>: add 0x1bad(%eax),%dh
建立新映射后 的第一条指令是:
mov $relocated, %eax
这时的eax是:
(gdb) info registers
eax 0xf010002f -267386833
格式化打印到控制台:
-
kern/printf.c
内核的cprintf控制台输出的简单实现,
基于printfmt()和内核控制台的cputchar()。 -
lib/printfmt.c
// 精简的基本printf样式格式化例程,
// 被printf,sprintf,fprintf等共同使用
// 内核和用户程序也使用此代码。
#include <inc/types.h>
#include <inc/stdio.h>
#include <inc/string.h>
#include <inc/stdarg.h>
#include <inc/error.h>
/*
* 数字支持空格或零填充和字段宽度格式。
*
*
* 特殊格式%e带有整数错误代码
* 并输出描述错误的字符串。
* 整数可以是正数或负数,
* ,使-E_NO_MEM和E_NO_MEM等效。
*/
static const char * const error_string[MAXERROR] =
{
[E_UNSPECIFIED] = "unspecified error",
[E_BAD_ENV] = "bad environment",
[E_INVAL] = "invalid parameter",
[E_NO_MEM] = "out of memory",
[E_NO_FREE_ENV] = "out of environments",
[E_FAULT] = "segmentation fault",
};
/*
* 使用指定的putch函数和关联的指针putdat
* 以相反的顺序打印数字(基数<= 16).
*/
static void
printnum(void (*putch)(int, void*), void *putdat,
unsigned long long num, unsigned base,