LAB1 启动操作系统
从机器上电到运行OS发生了什么?
在电脑主板上有一个Flash块,存放了BIOS的可执行代码。它是ROM,断电不会丢掉数据。在机器上电的时候,CPU要求内存控制器从0地址读取数据(程序第一条指令)的时候,内存控制器去主板上的BIOS所在ROM读取数据,此时CPU运行着BIOS。这里BIOS主要做了以下3个任务:
- 检测存在的硬件,并测试其是否正常工作。
- 初始化显卡、显存,检验视频信号和同步信号,对显示器接口进行测试。
- 根据配置选择某个外存(U盘、CD-ROM、硬盘这些)作为启动,将其第一个扇区(BootLoader默认在存储器的第一个扇区)加载到内存上某固定区段,然后设置CPU的CS:IP寄存器指向这个内存区域的起点。此时CPU运行着BootLoader。
在JOS实验中, BootLoader的源代码是boot/boot.S和boot/main.c。经过编译链接得到ELF格式的二进制文件obj/boot/boot。这便是存放在0号扇区里的BootLoader。
BootLoader会完成两个主要功能:
- BootLoader将处理器从实模式转换为保护模式。这样CPU就支持去寻址更大的内存空间。
- BootLoader使用x86特定的IO指令直接访问IDE磁盘设备寄存器,从外存加载内核(也就是OS)到内存上,并设置CPU的CS:IP寄存器指向这个内存区域的起点,此时CPU正式开始运行操作系统。
Questions
At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
在boot/boot.S中,计算机首先工作于16bit工作模式(实模式),当运行完 " ljmp $PROT_MODE_CSEG, $protcseg "语句后,正式进入32位工作模式(保护模式)。
What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?
- bootmain子程序的最后一条语句
((void (*)(void)) (ELFHDR->e_entry))();
,即跳转到操作系统内核程序的起始指令处。 - 第一条指令位于/kern/entry.S。为第一句
movw $0x1234, 0x472
。
How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?
- 操作系统文件中的Program Header Table存储了操作系统一共有哪些段,每个段有多少扇区等信息。每个表项对应操作系统一个段。找到这个表后即可确定操作系统内核占用了多少个扇区。
- 操作系统内核映像文件的ELF头部信息记录了这个表的存储位置。
BootLoader加载操作系统内核的详细过程
在JOS实验中,操作系统内核最后编译得到的是一个二进制映像文件obj/kern/kernel,这个文件就是UNIX标准下的ELF格式文件。
在JOS实验中,可以简单地认为obj/kern/kernel由三部分组成:
- 带有加载信息的文件头
- 程序段表
- 几个程序段
大致如下图所示:
这里使用objdump -x obj/kern/kernel
查看JOS内核的程序段表和所有段信息:
VMA即链接地址,这个段希望被存放到的逻辑地址。
LMA即加载地址,这个段被加载到内存中后所在的物理地址。
BootLoader首先将ELF的header从外存加载到内存上,
然后根据程序段表依次将需要加载的程序段从外存加载到内存上。
最后将CPU的CS:IP设置成操作系统内核的入口位置,操作系统内核正式启动。
内核准备就绪
在JOS实验中,JOS内核的入口点的源代码是/kern/entry.S的39行,从39行到77行这部分先开启了paging,后初始化了堆栈。然后转移到C语言写的i386_init。
内核的内存机制
内核的设计者希望为用户提供尽量大的内存空间,但是RAM的物理空间大小就那么大,怎么办,设计一套段页内存机制出来。paging是操作系统软件内部设计,而“保护模式”是CPU硬件工作模式。
前面在读程序段表的时候有两个属性,VMA和LMA。LMA是提供给BootLoader的,BootLoader根据LMA将内核的段们加载到内存的指定位置(就是段的LMA)。 VMA是提供给内核看的。
从软件视角(内核的安排设计)的内存(虚拟内存)上看,kernel被加载到高位地址空间上,低位地址空间留给上层应用使用.堆栈内存就在这里.
在JOS实验中,我们使用GDB的si从内核入口0x10000C开始调试,会碰到一条指令movl %eax, %cr0
,这条指令打开paging,从而支持虚拟地址。
Problems / 动手实现printf格式化输出到屏幕
- Explain the interface between printf.c and console.c. Specifically, what function does console.c export? How is this function used by printf.c?
console.c中实现了一些基础显示函数,供外部使用. printf.c中的cprintf()实现依赖于vcprintf()的实现,vcprintf()的实现依赖于putch()的实现,putch()的实现依赖于console.c提供的cputchar().
- Explain the following from 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_buf: 一个字符数组缓冲区,里面存放着要显示到屏幕上的字符.
变量crt_pos: 当前最后一个字符显示在屏幕上的位置.
给出的代码是cga_putc的中间部分,cga_putc的上部分是根据字符值int c来判断到底要显示成什么样子. cpga_putc的下部分则把决定要显示的字符显示到屏幕指定位置.
- For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC's calling convention on the x86.
Trace the execution of the following code step-by-step:
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
- In the call to cprintf(), to what does fmt point? To what does ap point?
- List (in order of execution) each call to cons_putc, va_arg, and vcprintf. For cons_putc, list its argument as well. For va_arg, list what ap points to before and after the call. For vcprintf list the values of its two arguments.
- Run the following code.
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
What is the output? Explain how this output is arrived at in the step-by-step manner of the previous exercise. Here's an ASCII table that maps bytes to characters.
The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?
Here's a description of little- and big-endian and a more whimsical description.
(PASS)
- In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?
cprintf("x=%d y=%d", 3);
(PASS)
- Let's say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change cprintf or its interface so that it would still be possible to pass it a variable number of arguments?
The stack
kernel从哪条指令开始初始化堆栈?
kern/entry.S中,
call i386_init
指令前的这两句:
movl $0x0,%ebp # nuke frame pointer
movl $(bootstacktop),%esp
JOS堆栈位于内存的什么位置?
kern/entry.S中的这几句初始化了JOS内核的分页机制:
1 movl $(RELOC(entry_pgdir)), %eax
2 movl %eax, %cr3
3 movl %cr0, %eax
4 orl $(CR0_PE|CR0_PG|CR0_WP), %eax
5 movl %eax, %cr0
第1、2句,把entry_pgdir页表的起始地址送入%eax寄存器和%cr3寄存器
第3、4、5句,修改cr0寄存器的值,把cr0的PE位,PG位, WP位都置位1。其中PE位是启用保护标识位,如果被置1代表将会运行在保护模式下。PG位是分页标识位,如果这一位被置1,则代表开启了分页机制。WP位是写保护标识,如果被置位为1,则处理器会禁止超级用户程序向用户级只读页面执行写操作。
紧接着的下面这两句
1 mov $relocated, %eax
2 jmp *%eax
把当前运行程序的地址空间提高到[0xf0000000-0xf0400000]范围内。
然后
1 movl $0x0,%ebp # nuke frame pointer
2 movl $(bootstacktop),%esp
3 call i386_init
在entry.S的末尾还定义了一个值,bootstack。注意,在数据段中定义栈顶bootstacktop之前,首先分配了KSTKSIZE这么多的存储空间,专门用于堆栈,这个KSTKSIZE = 8 * PGSIZE = 8 * 4096 = 32KB。所以用于堆栈的地址空间为 0xf0108000-0xf0110000,其中栈顶指针指向0xf0110000. 那么这个堆栈实际坐落在内存的 0x00108000-0x00110000物理地址空间中。