MIT6.828-Lab1-总结
计算机启动步骤
本lab主要告诉我们如何启动一台计算机,其启动流程分以下三步:
1.BIOS
- 首先进行硬件自检(Power-On Self-Test),检查硬件能否满足运行的基本条件
- 根据启动顺序,读取优先级最高的存储设备,(启动顺序可以在BIOS界面中设置)
- 依次读取扇区(512字节),若最后两个字节为
0x55
和0xAA
,则表明此扇区为引导扇区,随后将此扇区的512字节读入0x7c00
处,此段程序即为bootloader
那么,BIOS的第一条指令为什么在0xffff0处呢?
因为不同厂家的BIOS程序大小不一,所以如果将BIOS放在0x0000
处,会导致用户可用的内存不是从0x0000
开始,并且8086规定,CPU必须先从0xffff0
开始,并且0xffff0
处必须是一条无条件转移指令JMP用于跳转到BIOS所在的位置。
2.BootLoader-boot.S
- 打开A20地址线以便于访问超过1M的地址
- 构建并加载GDT
- 寻址模式从
real-mode
转变为32位protected-mode
(real-mode
用段寄存器里存放的是段基址,但protected-mode
中段寄存器里存放的是段选择子,使用段选择子在全局描述符表中获取段基址,所以在此过程中可以加一道特权级检查)
为什么BootLoader被加载到内存
0x7c00
的位置呢?
因为Intel最早的第一台个人电脑芯片8088,搭配的操作系统86-DOS,此操作系统占用内存32KB, 同时芯片本身占用了0x0000-0x03ff
的位置用于保存中断处理程序的地址,故只剩下了0x0400-0x7fff
, 考虑到在OS被加载到内存中后,BootLoader
就不会再被用到了,所以把BootLoader加载到0x7fff - 512B - 512B
的位置处便于后续操作系统利用这片内存。(两个512B是因为考虑到BootLoader
也会产生数据)
验证BootLoader
0x7c00-0x7dff
属于BootLoader
,0x7e00-0x7fff
属于相关数据,使用以下命令检查引导扇区的特征字节
3.BootLoader-main.c
- 将kernel的ELF文件读入到物理地址
0x1000
处 - 根据ELF文件跳转到内核代码(load address在
0x100000
,入口在0x10000c
,这两个数值是在kern/kernel.ld
中通过ENTRY(_start)
和.text : AT(0x100000)
指定的),控制权交给内核1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct Proghdr *ph, *eph;
// read 1st page off disk
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// is this a valid ELF?ELF
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff); // e_phoff表示程序头表相对于ELF头的偏移地址
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!
// 使用e_entry跳转到内核代码
((void (*)(void)) (ELFHDR->e_entry))();
ELF格式简介
- 我们上面是将kernel的ELF头加载到
0x1000
处了,ELF头主要有三个信息: 程序入口地址相关e_entry
、程序头表相对于ELF头的偏移地址e_phoff
、程序头个数e_phnum
- 程序头(programheader,缩写为Proghdr),同样主要有三个信息:程序头类型(或者说段类型)
p_type
、程序段在硬盘中的位置p_offset
、程序段在内存的物理地址和虚拟地址p_pa
,p_va
- 使用
readelf -l obj\kern\kernel
查看kernel的所有程序头信息 - 使用
readelf -h obj/kern/kernel
查看程序头表(也可以叫ELF头)
4.内核!启动!- entry.S
- 创建临时页表,将虚拟地址[0, 4MB)和[KERNBASE, KERNBASE + 4MB)的位置全部映射到物理地址[0, 4MB),这样子在后面可以既在低地址运行也可以在高地址运行内核(因为需要通过reloc才能到高地址运行内核)
- 将
entry_pgdir
的地址赋值给cr3寄存器,并开启cr0寄存器的PG位,这样子内核就可以使用我们定义的页表了
5.call i386_init函数
- 操作系统的各种初始化,初始化控制台,内存管理,进程初始化,中断向量表初始化等等
GCC calling convention for JOS
让我们看下面这段汇编代码,请记住,在call时,实际上进行两个指令:push %eip
和movl $0x12345, %eip
1 |
|
每当进入函数时,便会执行两条指令,push %ebp
和mov %esp %ebp
(和上面代码不一样是因为我写的是AT&T语法),ebp寄存器用于保存当前所执行的函数的基址,因为我们知道在执行函数是esp的值会不断减小,所以我们需要使用ebp来保存esp变化之前的值,以便于我们读取函数参数。
下面是我画的每次函数执行时,stack的部分图片:
目前为止,物理内存里长啥样?
tips
从一开始的配置环境,到最后完成整个lab,最大的印象是无论如何都要认真地看完每一段话,我仍记得当我发现首次进入内核时地址为0x10000c
时的疑惑,直到后面Exercise 4时,要求我们使用objdump -f obj/kern/kernel
来verify时,方能恍然大悟;我仍记得想破脑袋也没想明白为什么bootloader
从0x7c00
开始,直到查阅相关资料才发现这只是历史遗留问题 。
- 想要真正读懂bootloader(boot.S和main.c)还是得去认真地把xv6-book的附录B部分过一遍。
- 在entry.S中有一行指令
mov $relocated, %eax
,直到此命令执行结束后,内核代码才真正地在高memory地区执行,在此之前,如果对高地址进行breakpoint是没有任何效果的。 - 关于GCC calling conventions for JOS的部分一定要着重理解,只有了解的足够深,才能意识到为什么eip可以通过
*((uint32_t *)ebp + 1)
来获取(与call指令本质上的push %eip
有关),也才能意识到C语言中函数参数是从按照声明顺序从后往前push入栈的。 - 关于backtrace中的
stab.h
部分,n_value
是用来表示符号地址的(虽然不知道为什么注释要写// value of symbol
),n_desc
对于N_SLINE
类型是表示行号的(虽然只看注释我也看不出来,但我猜对了)。