MIT6.828(一)Lab1:Booting a PC

获取完资源就开始干了。
我这里就做了个大概的内容分析,具体的还是跟着那几个大佬吧

前面一大堆都是没用的,就是告诉一些作业的提交。

Part 1: PC Bootstrap

这个并没有要你做啥,就是让你 熟悉下汇编。

然后让你知道怎么运行那个内核的,这些都是些不用讲的东西,看看就行了。
说一下,这个内核退出说是ctrl+a x,意思是先按ctrl+a,再按x

然后就开始来重点了,第一个是让你知道现在计算机内存的分布。

The PC’s Physical Address Space(PC的物理地址空间)

在这里插入图片描述

The ROM BIOS

后面就比较直接了,直接让你去运行这个内核,一步步来看他是怎么运行的。

在这里插入图片描述
就是让你这样运行两个终端,跑GDB 调试,第一个运行make qemu-gdb,第二个运行make gdb
不出意外,在make gdb 里面出现的第一条指令是跳转指令。
在这里插入图片描述
跳到了哪里去,这个地址官方是给了你解释的,就是段地址*16加偏移地纸,很显然,他的地址是在BIOS中。
然后回让你一步步执行看发生了什么。
不难发现,计算机的运行最先开始的是 BIOS。

后面一步步,执行的内容我就看不懂,只知道他就做了一些初始化工作,想了解的去看看大佬的博客吧。

Part 2: The Boot Loader

初始化完成BIOS 之后,就运行Boot Loader,就是引导操作系统,这个东西一直都是放在计算机磁盘的第一个扇区(也用可能是其他的引导,这个百度搜一搜能搜出来,另外一个最常见的就是U盘启动,学计算机的重装系统至少也有十次八次了吧,所以这个很容易理解,我们重装系统的步骤不就是现在BIOS里面选了U盘引导,干的就是这个了)。然后BIOS会把这个运行的引导程序装入到内存0x7c00-0x7dff。我不知道现代操作系统是不是这,但是这个系统是的。
这个引导系统主要就干了两件事:

  1. 实模式转换成保护模式,区别百度一下有讲解的,另外发现了另一个操作系统学习资源,好像文档还挺齐全的。
  2. 引导加载程序通过x86的特殊I / O指令直接访问IDE磁盘设备寄存器,从而从硬盘读取内核。

后面会让你去看源码,知道怎么运行的Boot Loader,这个各位就自己翻翻博客吧,我就不细讲了,贴个自己的大致介绍。

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
#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
# 简单来讲 上面 就是初始化了一些寄存器,然后就去 boot main 里面了
# 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

由上面这注释,应该很容易得出他问的问题的答案。

后面有一个让你运行一个C语言指针程序,那个有点基础应该就看得懂。就不多说了。

main.c里面,主要是加载ELF文件,也就是你的操作系统,什么是ELF,这个可以在百度百科里面了解到。

boot main.c

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <inc/x86.h>
#include <inc/elf.h>

/**********************************************************************
* This a dirt simple boot loader, whose sole job is to boot
* an ELF kernel image from the first IDE hard disk.
*
* DISK LAYOUT
* * This program(boot.S and main.c) is the bootloader. It should
* be stored in the first sector of the disk.
*
* * The 2nd sector onward holds the kernel image.
*
* * The kernel image must be in ELF format.
*
* BOOT UP STEPS
* * when the CPU boots it loads the BIOS into memory and executes it
*
* * the BIOS intializes devices, sets of the interrupt routines, and
* reads the first sector of the boot device(e.g., hard-drive)
* into memory and jumps to it.
*
* * Assuming this boot loader is stored in the first sector of the
* hard-drive, this code takes over...
*
* * control starts in boot.S -- which sets up protected mode,
* and a stack so C code then run, then calls bootmain()
*
* * bootmain() in this file takes over, reads in the kernel and jumps to it.
**********************************************************************/

#define SECTSIZE 512
#define ELFHDR ((struct Elf *) 0x10000) // scratch space

void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);

void
bootmain(void)
{
struct Proghdr *ph, *eph;

// read 1st page off disk 把内核的起始地址第一个页加载到内存,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 */;
}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
// 顾名思义 ,就是加载 偏移量为 offset 的 连续 count 个字节 到地址 pa
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
uint32_t end_pa;

end_pa = pa + count;

// round down to sector boundary
pa &= ~(SECTSIZE - 1);

// translate from bytes to sectors, and kernel starts at sector 1
offset = (offset / SECTSIZE) + 1;

// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
while (pa < end_pa) {
// Since we haven't enabled paging yet and we're using
// an identity segment mapping (see boot.S), we can
// use physical addresses directly. This won't be the
// case once JOS enables the MMU.
readsect((uint8_t*) pa, offset);
pa += SECTSIZE;
offset++;
}
}

void
waitdisk(void)
{ // 判磁盘是不是准备 好了
// wait for disk reaady
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}

void
readsect(void *dst, uint32_t offset)
{
// wait for disk to be ready
waitdisk();

outb(0x1F2, 1); // count = 1
outb(0x1F3, offset);
outb(0x1F4, offset >> 8);
outb(0x1F5, offset >> 16);
outb(0x1F6, (offset >> 24) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors

// wait for disk to be ready
waitdisk();

// read a sector 读一个扇区
insl(0x1F0, dst, SECTSIZE/4);
}

后面几步还是要自己好好坐一坐,我没有做什么详细的介绍。
一如既往大佬博客,像我这样的菜鸡是无法理解的。

最后让你做个测试,运行Boot Loader 前看下0x00100000处的8个内存字,运行后再看一下,发现原本是 全是 0,后面就有了一大堆乱七八糟的值。这个不是很明显吗,看我上面main.c的注释,明显有个函数把硬盘里面的值读到了内存。

Part 3: The Kernel

后面的实验,这个大佬的GitHub就全都有了。

我做个简单的笔记。

1
movl %eax, %cr0

这条语句实现了,映射,将高地址映射到了物理地址的低地址,这是因为计算机希望操作系统是运行在高地址,用户运行在低地址,详细内容,下一次实验会讲清楚。

练习8 让你完善那个printf里面的%o也就是8进制输出。

这个很容易了,把前面16进制输出复制一遍就行了,不做详细介绍了。

然后你需要回答4个问题

  1. printf之间的关系依旧找大佬的博客
  2. 1
    2
    3
    4
    5
    6
    7
    1      if (crt_pos >= CRT_SIZE) {
    2 int i;
    3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
    4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
    5 crt_buf[i] = 0x0700 | ' ';
    6 crt_pos -= CRT_COLS;
    7 }

解释这个,看起来就是如果超过了一页,就把最前面的那一行删掉??,然后继续输出下一行???不知道理解对没。

  1. 这个是个重点 ,这个实验很容易看出来C语言压栈的顺序是从后往前压栈。
  2. 这个不说了,就是按格式输出
  3. 这个就是,我们经常用的printf,如果少了一个参数会咋样,通过看内存应该就知道,会输出一个随机值。多一个多的其实就没啥用。
  4. 这个就是压栈是从后往前压,所以可以实现,多个参数。如果是从前往后压该怎么办?我还不知道怎么处理。

练习9 问啥时候初始化堆栈,这个看看大佬的博客就行.
练习10 这个就是让你明白是怎么调用函数的,又是怎么返回的。

一般来说,栈基地址指向的是栈底,栈指针指向的是栈顶。
在这里插入图片描述
大概调用一个函数就是这么干的,怎么返回的就不用我说了吧。
调用参数也就是通过栈来的,如果超过了五个参数我也不知道会发生啥,反正这上面没说。
练习 11 也就是让你写个输出栈里面的内容。

练习 12 让你实现一个调试信息的指令。
改三个地方。
前两个都在kern/monitor.c
第一个

1
2
3
4
5
static struct Command commands[] = {
{ "help", "Display this list of commands", mon_help },
{ "kerninfo", "Display information about the kernel", mon_kerninfo },
{"backtrace","Display stack backtrace", mon_backtrace},
};

后面那个是多加的,前面那两个很眼熟吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
// Your code here.
// return 0;
uint32_t ebp,eip,*p;
struct Eipdebuginfo info;
ebp=read_ebp();
while(ebp!=0){
p=(uint32_t*)ebp;
eip=p[1];
cprintf("ebp %x eip %x args %08x %08x %08x %08x %08x\n",ebp,p[1],p[2],p[3],p[4],p[5],p[6]);
if(debuginfo_eip(eip,&info)==0){
int fn_offset=eip-info.eip_fn_addr;
cprintf("%s:%d:%.*s+%d\n",info.eip_file,info.eip_line,info.eip_fn_namelen,info.eip_fn_name,fn_offset);
}
ebp=p[0];
}
return 0;
}

这个也很眼熟吧,前面改过。
到这个地方实际上已经能够运行了,只是没有行号。

最后一个在kern/kdebug.c

1
2
3
4
5
6
stab_binsearch(stabs, &lfun, &rfun, N_SLINE, addr - info->eip_fn_add    r);

if (lfun <= rfun)
{
info->eip_line = stabs[lfun].n_desc;
}

这个具体原理我也不知道,抄别人的。百度能搜到
到此,实验一就全部完成了,目前我能了解的就这么多。