Contents
Lab 1: Booting a PC
Part 1: PC Bootstrap
The PC’s Physical Address Space
PC的物理空间如下
+—————————+ <– 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
最重要的就是64kb的BIOS区域,在操作系统启动时首先会运行这一部分,进行基本的初始化然后从软盘、硬盘或者CD上面读操作系统到内存里面。
用Gdb调试,首先运行的就是
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
它的地址是0xffff0,在BIOS区域的最上方。第一步就是跳到0xfe05b执行后续的代码
Exercise 2.
通过GDBsi进行调试,猜测BIOS进行了什么操作。
这是BIOS的部分代码
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
[f000:e05b] 0xfe05b: cmpl $0x0,%cs:0x6ac8
[f000:e062] 0xfe062: jne 0xfd2e1
[f000:e066] 0xfe066: xor %dx,%dx
[f000:e068] 0xfe068: mov %dx,%ss
[f000:e06a] 0xfe06a: mov $0x7000,%esp
[f000:e070] 0xfe070: mov $0xf34c2,%edx
[f000:e076] 0xfe076: jmp 0xfd15c
[f000:d15c] 0xfd15c: mov %eax,%ecx
[f000:d15f] 0xfd15f: cli
[f000:d160] 0xfd160: cld
[f000:d161] 0xfd161: mov $0x8f,%eax
[f000:d167] 0xfd167: out %al,$0x70
[f000:d169] 0xfd169: in $0x71,%al
[f000:d16b] 0xfd16b: in $0x92,%al
[f000:d16d] 0xfd16d: or $0x2,%al
[f000:d16f] 0xfd16f: out %al,$0x92
[f000:d171] 0xfd171: lidtw %cs:0x6ab8
[f000:d177] 0xfd177: lgdtw %cs:0x6a74
[f000:d17d] 0xfd17d: mov %cr0,%eax
[f000:d180] 0xfd180: or $0x1,%eax
[f000:d184] 0xfd184: mov %eax,%cr0
[f000:d187] 0xfd187: ljmpl $0x8,$0xfd18f
主要是设置了ss、esp寄存器;通过cli屏蔽中断,通过IN、OUT与IO设备交互,打开A20门,设置Cr0寄存器,进入实模式。跳转到内核再执行后续的部分。
后续部分看别人的说法还有,但是太麻烦了就不想看了。
- 当BIOS在运行的时候。会在内存的超始地址处建立一个各种设备的16位的中断向量表,并且初始化各种设备。利用这个中断向量表,就可以成功输出”Start SeaBIOS”这种信息。
- 当各种初始化的硬件设备工作做完之后。BIOS就开始找启动设备,最终会在硬盘的起始sector里面找0x55, 0xaa这标志位的扇区。如果有,就加载到0x7c00处开始运行。也就是(31KB)的位置。
Part 2: The Boot Loader
BIOS初始化完之后,将Boot Loader读到内容的0x7c00处,然后跳转到0x7c00执行Boot Loader。
Boot Loader主要干两件事:
1.从实模式切换到32位保护模式
2.将系统内核从硬盘读取到内存中
Boot Loader对应的代码为 boot/boot.S、boot/main.c , 编译后的代码在obj/boot/boot.asm,JOS内核编译后代码在obj/kern/kernel.asm
Exercise 3.
- 处理器什么时候开始执行 32 位代码?究竟是什么原因导致从 16 位模式切换到 32 位模式?
- 引导加载程序执行的最后一条指令是什么,刚刚加载的内核的第一条指令是什么?
- 内核的第一条指令在哪里?
- 引导加载程序如何决定必须读取多少个扇区才能从磁盘获取整个内核?它在哪里找到这些信息?
来看boot.S代码
首先cli禁用中断,然后清空ds、es、ss寄存器
之后是开启A20门。当A20门关闭是,CPU总线只能接收16位的寄存器,使用20位的内存,只有开启A20门后,CPU才能以32位模式使用
接着,从实模式跳到保护模式,然后运行32位的代码。这里lgdt不是特别清除,之后通过修改cr0寄存器,从实模式进入保护模式。最后通过一个跳转,执行32位代码。
32 位代码里面首先是设置段寄存器、设置栈顶然后跳到bootmain里面。这里的start就是0x7c00
main.c
接着看main.c
首先把kernel的头部读到内存中
通过头部的魔数判断,当前是否位elf文件
通过elf_header获得当前elf存在几个program_header
通过program_header中的mem和offset确定读取的偏移和大小
读取完所有的program_header后,通过一个函数指针e_entry进入主程序
接着继续看readseg怎么处理的
readseg表示从offset读取count bytes数据到pa的地址
首先确定结束地址end_pa
pa向下对齐到512
将offset转化为sector的位置,这里+1是因为Boot Loader在第一个sector里面
之后就是如果pa<end_pa不断读取offset位置的sector,每次读取完一个sector,pa + 512.
readsect具体的汇编就不细看了,是对于具体的IO端口操作细节
Boot Loader的最后call *0x10018 ,也就是 elfheader -> entry的地址进入kernel
进去的第一条指令是
0x10000c: movw $0x1234,0x472
到这里Boot Loader就看完了
Exercise 3-Anser.
- 处理器什么时候开始执行 32 位代码?究竟是什么原因导致从 16 位模式切换到 32 位模式?
程序从一个 ljmp $PROT_MODE_CSEG, $protcseg 跳转开始执行32位代码
切换到32位模式原因是修改了CR0寄存器 movl %eax, %cr0
- 引导加载程序执行的最后一条指令是什么,刚刚加载的内核的第一条指令是什么?
call bootmain
0x10000c: movw $0x1234,0x472
- 内核的第一条指令在哪里?
0x10000c:
- 引导加载程序如何决定必须读取多少个扇区才能从磁盘获取整个内核?它在哪里找到这些信息?
通过program_header中的mem计算出对应读取的扇区数量,offset计算扇区起始位置
Exercise 5.
修改boot/Makefrag文件中的地址,通过GDB调试判断Boot Loader中哪几条命令会出问题。
这里把0x7c00改为0x8c00.
原始的汇编代码为
0x7c01: cld
0x7c02: xor %ax,%ax
0x7c04: mov %ax,%ds
0x7c06: mov %ax,%es
0x7c08: mov %ax,%ss
0x7c0a: in $0x64,%al
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
0x7c10: mov $0xd1,%al
0x7c12: out %al,$0x64
0x7c14: in $0x64,%al
0x7c16: test $0x2,%al
0x7c18: jne 0x7c14
0x7c1a: mov $0xdf,%al
0x7c1c: out %al,$0x60
0x7c1e: lgdtw 0x7c64
0x7c23: mov %cr0,%eax
0x7c26: or $0x1,%eax
0x7c2a: mov %eax,%cr0
0x7c2d: ljmp $0x8,$0x7c32
修改链接的起始地址为0x8c00后
0x7c01: cld
0x7c02: xor %ax,%ax
0x7c04: mov %ax,%ds
0x7c06: mov %ax,%es
0x7c08: mov %ax,%ss
0x7c0a: in $0x64,%al
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
0x7c10: mov $0xd1,%al
0x7c12: out %al,$0x64
0x7c14: in $0x64,%al
0x7c16: test $0x2,%al
0x7c18: jne 0x7c14
0x7c1a: mov $0xdf,%al
0x7c1c: out %al,$0x60
0x7c1e: lgdtw –0x739c
0x7c23: mov %cr0,%eax
0x7c26: or $0x1,%eax
0x7c2a: mov %eax,%cr0
0x7c2d: ljmp $0x8,$0x8c32
其中lgdtw和ljmp对应的地址与0x7c00不同,而且将链接的start地址改为0x8c00,但是BIOS仍然将Boot Loader 加载到0x7c00的位置。这就导致程序中jmp的地址与实际地址不符。
Exercise 6
使用GDB在BIOS进入Boot Loader和Boot Loader进入Kernel时查看0x00100000的8个字节,为什么会不同,在进入Kernel时内存中是什么。
进入Boot Loader时内存
进入Kernel时的内存
实际上0x100000的值就是Entry.s的的内容,在Boot Loader时将Kernel读入到内存中。
Part 3: The Kernel
一般来说操作系统喜欢将系统内核加载到很高的虚拟的地址如0xf0100000,这就就将低地址留给用户使用。但是机器不一定在0xf0100000有内存。实际上是将0xf0100000的地址映射到
0x00100000处,也就是1MB的位置,刚好在BIOS的上方。就算是对于早期的PC,1MB的物理内存也是有的。
在下一个实验中就会将0x00000000 到 0x0fffffff 256MB的内存映射到 0xf0100000到0xf0100000,这也就是为什么说JOS只能用256MB的内存。
现在只需要映射前4MB的内存就足够启动并运行了,在 kern/entrypgdir.c 中使用手写的静态Page Diractory和Page table来实现。
在kern/entry.S设置CR0标志位之前,内存使用的就是物理地址,设置了CR0标志位后 entry_pgdir 将0xf0000000 到0xf0400000 以及 0x00000000 through 0x00400000 都映射到了 0x00000000 到 0x00400000的物理地址,如果使用这两个范围之外的内存地址,就会导致异常,因为没有设置中断,出现异常会qemu就会dump当前状态之后退出
Exercise 7
在 movl %eax, %cr0 下断检查 0x00100000 和 0xf0100000的内存,执行movl %eax, %cr0后再次查看,如果把movl %eax, %cr0 注释掉,那一条指令会最先出现问题。
在执行完 movl %eax, %cro前0xf0100000内存为空都是 0,执行后两个地址内存一样
在jmp *%eax出现了问题,也就是这里,relocated地址为0xf010002c,jmp过去的时候出现了问题。
Formatted Printing to the Console
printf在C语言中如此常见,以至于将它当作C中的原语,但是在JOS中需要自己实现所有的I/O。
通读kern/printf.c lib/printfmt.c 和 kern/console.c。
Exercise 8
省略了printf中%o以八进制输出的代码,找到并补全代码。
在kern/printfmt.c中找到vprintfmt函数,其中207行就是case ‘o’,按照case’x’的格式补全就行
—– a/lib/printfmt.c
+++ b/lib/printfmt.c
@@ –206,10 +206,9 @@ vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)
// (unsigned) octal
case ‘o’:
// Replace this with your code.
– putch(‘X’, putdat);
– putch(‘X’, putdat);
– putch(‘X’, putdat);
– break;
+ num = getuint(&ap, lflag);
+ base = 8;
+ goto number;
// pointer
case ‘p’:
回答以下问题:
1. printf.c和console.c的接口,console.c导出了什么函数,这个函数如何被printf.c使用。
cprintf -> vcprintf -> vprintfmt-> putch -> cputchar
console.c导出了cputchar给printf.c使用
2. 解释以下console.c中的代码
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 }
CRT_SIZE是屏幕能够输出的所有字符个数, crt_pos>=CRT_SIZE就表示当前输出的字符已经将当前屏幕输出满。CRT_COLS指一行的字符个数,memmove表示将2-n行复制到1~n-1行,然后最后一行输出黑色的空格。再将光标移动到上一行。
3. 这里就不按照题目来了,直接看printf中如何实现可变变量长度的。
1 | int |
2 | cprintf(const char *fmt, ...) |
3 | { |
4 | va_list ap; |
5 | int cnt; |
6 | |
7 | va_start(ap, fmt); |
8 | cnt = vcprintf(fmt, ap); |
9 | va_end(ap); |
10 | |
11 | return cnt; |
12 | } |