0%

MIT-6.828-JOS 笔记 - Lab 1: Booting a PC

本实验主要是熟悉汇编、系统启动过程等。

环境配置参考:https://github.com/woai3c/MIT6.828/blob/master/docs/install.md

本实验代码:https://github.com/epis2048/MIT_6.828_2018/tree/lab1

一、系统启动

本部分不需要动手,只需要动脑了解内核是怎么运行的。

The PC’s Physical Address Space

物理地址空间示意:

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
+------------------+  <- 0xFFFFFFFF (4GB)
| 32-bit |
| memory mapped |
| devices |
| |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
| |
| Unused |
| |
+------------------+ <- depends on amount of RAM
| |
| |
| Extended Memory |
| |
| |
+------------------+ <- 0x00100000 (1MB)
| BIOS ROM |
+------------------+ <- 0x000F0000 (960KB)
| 16-bit devices, |
| expansion ROMs |
+------------------+ <- 0x000C0000 (768KB)
| VGA Display |
+------------------+ <- 0x000A0000 (640KB)
| |
| Low Memory |
| |
+------------------+ <- 0x00000000

现代系统已经扩展到了4GB,BIOS放在最上面。
1MB以下空间是为了与过去兼容,现代计算机已经不用了。

二、引导加载器

初始化完成BIOS 之后,就运行Boot Loader,就是引导操作系统。引导系统主要两件事:

  1. 把实模式转换为保护模式。
  2. 通过x86的特殊I / O指令直接访问IDE磁盘设备寄存器,从而从硬盘读取内核。
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// boot/boot.S
#include <inc/mmu.h>

# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag

.globl start
start: # 开始程序首先关了中断
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
#初始化数据段、扩展段和栈的段寄存器,用于保护模式
# Set up the important data segment registers (DS, ES, SS).
xorw %ax,%ax # Segment number zero
movw %ax,%ds # -> Data Segment
movw %ax,%es # -> Extra Segment
movw %ax,%ss # -> Stack Segment

# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1: #开启A20为了兼容,低版本的处理器
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.1

movb $0xd1,%al # 0xd1 -> port 0x64
outb %al,$0x64

seta20.2:
inb $0x64,%al # Wait for not busy
testb $0x2,%al
jnz seta20.2

movb $0xdf,%al # 0xdf -> port 0x60
outb %al,$0x60

# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to their physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc # 存放 GDT表信息
movl %cr0, %eax # 用或操作 把最后一位置1,开启保护模式
orl $CR0_PE_ON, %eax
movl %eax, %cr0

# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg # 这个时候跳到了32模式下了

.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# 简单来讲,上面就是初始化了一些寄存器,然后就去bootmain 里面了
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain

# If bootmain returns (it shouldn't), loop.
spin: # 这个翻译上面英文就好
jmp spin

# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt: # GDT 表信息
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG(STA_W, 0x0, 0xffffffff) # data seg

gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
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
// boot/main.c
void
bootmain(void)
{
struct Proghdr *ph, *eph;

// 把内核的起始地址第一个页加载到内存,ELFHDR处 一页为 512*8=4M
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0); // 一般是操作系统映象文件的elf 头部

// is this a valid ELF? 是一个ELF文件就继续否侧失败
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// 加载程序表头到 ph
// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum; // 这个是表未e_phnum 存的是表项个数
for (; ph < eph; ph++) // 这个就是把表里面的都加载到内存
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))(); // e_entry 是程序运行的入口 也就是在这个时候操作系统开始加载了

bad: // 没有加载到系统的操作,
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}

三、内核

1. 开启分页模式

操作系统经常被加载到高虚拟地址处,比如0xf0100000,但是并不是所有机器都有这么大的物理内存。可以使用内存管理硬件做到将高地址虚拟地址映射到低地址物理内存。
虚拟地址的高10位(0000000010B)作为页目录的下标,从页目录中获取页表的物理地址0x08001000,虚拟地址的第11~20位(0000000001B)作为页表的下标,得到该页对应的物理地址0x0000c000,最后将虚拟地址的低12位(000001010000B或者0x50)和得到的页的物理地址(0x0000c000)加得到0x00000c050就是虚拟地址0x00801050转换后的物理地址。

系统将页目录的物理地址复制到cr3寄存器,并且将cr0 的最高位PG位设置为1后,正式打开分页功能。

2. 格式化输出到控制的台

往控制台写字符串,本质是往物理地址0xB8000开始的显存写数据。

Exercise 8
要求添加一些代码,使能支持”%o”输出八进制。在vprintfmt()中找到case ‘o’的地方:

1
2
3
4
5
6
7
// lib/printfmt.c
// (unsigned) octal
case 'o':
num = getuint(&ap, lflag);
//设置基数为8
base = 8;
goto number;

3. 栈

函数调用过程:

  1. 执行call指令前,函数调用者将参数入栈,按照函数列表从右到左的顺序入栈
  2. call指令会自动将当前eip入栈,ret指令将自动从栈中弹出该值到eip寄存器
  3. 被调用函数负责:将ebp入栈,esp的值赋给ebp。

Exercise 11-12
补全mon_backtrace()函数,本质上就算根据ebp一层层网上找。
实验提供了int debuginfo_eip(uintptr_t addr, struct Eipdebuginfo *info)函数(在/kern/kdebug.c中),该函数输入eip,和一个Eipdebuginfo结构指针,执行完毕后,会将eip对应的信息填充到该结构中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// kern/monitor.c
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
uint32_t *ebp = (uint32_t *)read_ebp();
struct Eipdebuginfo eipDebugInfo;
while(ebp != 0){
uint32_t eip = *(ebp + 1);
//打印ebp,eip,最近的五个参数
cprintf("ebp %08x eip %08x args %08x %08x %08x %08x %08x\n", ebp, eip, *(ebp + 2), *(ebp + 3), *(ebp + 4), *(ebp + 5), *(ebp + 6));
//打印文件名等
debuginfo_eip((uintptr_t)eip, &eipDebugInfo);
//cprintf("%d", eipDebugInfo.eip_fn_namelen);
cprintf("\t%s:%d: %.*s+%d\n", eipDebugInfo.eip_file, eipDebugInfo.eip_line, eipDebugInfo.eip_fn_namelen, eipDebugInfo.eip_fn_name, eip - eipDebugInfo.eip_fn_addr);
//cprintf(": %.*s+%d\n", eipdebuginfo.eip_fn_namelen, eipdebuginfo.eip_fn_name, eipdebuginfo.eip_fn_addr);
//更新ebp
ebp = (uint32_t *)(*ebp);
}
return 0;
}