Skip to content

Commit

Permalink
update appendix A and B, closes #50, closes #51, #40
Browse files Browse the repository at this point in the history
  • Loading branch information
ranxian committed Dec 23, 2015
1 parent f6f1c69 commit 473d910
Show file tree
Hide file tree
Showing 2 changed files with 17 additions and 16 deletions.
9 changes: 5 additions & 4 deletions content/AppendixA.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

###PC 硬件

本文介绍供 x86 运行的个人计算机(PC)硬件平台。
本文介绍供 x86 运行的个人计算机(PC)硬件平台。

PC 是指遵守一定工业标准的计算机,它的目标是使得不同厂家生产的机器都能够运行一定范围内的软件。这些标准随时时间迁移不断变化,因此90年代的 PC 与今日的 PC 看起来已是大不相同。

从外观来看,PC 是一个配置有键盘、屏幕和各种设备的“盒子”。盒子内部则是一块集成电路——*主板*,上面有 CPU 芯片,内存芯片,显卡芯片,I/O 控制器芯片,以及负责芯片间通信的总线。总线会遵守某种标准(如 PCI 或 USB),从而能够兼容不同厂家的设备。
从外观来看,PC 是一个配置有键盘、屏幕和各种设备(例如光驱)的“盒子”。盒子内部则是一块集成电路——*主板*,上面有 CPU 芯片,内存芯片,显卡芯片,I/O 控制器芯片,以及负责芯片间通信的总线。总线会遵守某种标准(如 PCI 或 USB),从而能够兼容不同厂家的设备。

我们可以把 PC 抽象为三部分:CPU、内存和 I/O 设备。CPU 负责计算,内存用于保存计算用的指令和数据,其他设备用于实现存储、通讯等其他功能。

你可以想象主存以一组导线与 CPU 相连接,其中一些是地址线,一些是数据线,还有一些则是控制线。CPU 要从主存读出一个值,需要向地址线上输出一系列表示0和1的电压,并在规定的时间内在 “读” 线上发出信号1,接下来再从数据线上的高低电压中获取数据。CPU 若要向内存中写入一个值,则向数据线和地址线上写入合适的值,并在规定时间内在 "写" 位上发出信号1.真实的内存接口比这复杂的多,但除非你是在追求高性能,否则你不必考虑这么多的细节。
你可以想象主存以一组导线与 CPU 相连接,其中一些是地址线,一些是数据线,还有一些则是控制线。CPU 要从主存读出一个值,需要向地址线上输出一系列表示0和1的电压,并在规定的时间内在 “读” 线上发出信号1,接下来再从数据线上的高低电压中获取数据。CPU 若要向内存中写入一个值,则向数据线和地址线上写入合适的值,并在规定时间内在 "写" 鲜上发出信号1.真实的内存接口比这复杂的多,但除非你是在追求高性能,否则你不必考虑这么多的细节。

### 处理器和内存

CPU(中央处理单元,或处理器)其实只是在执行一个非常简单的循环:从一个被称为『程序计数器』的寄存器中获取一个内存地址,从个地址读出机器指令,增加程序计数器的值,执行机器指令,不断反复。某些机器指令如分支和函数调用会改变程序计数器,如果执行机器指令没有改变程序计数器,这个循环就会从程序计数器开始一条一条地执行指令。
CPU(中央处理单元,或处理器)其实只是在执行一个非常简单的循环:从一个被称为*程序计数器
*的寄存器中获取一个内存地址,从个地址读出机器指令,增加程序计数器的值,执行机器指令。不断重复。某些机器指令如分支和函数调用会改变程序计数器,如果执行机器指令没有改变程序计数器,这个循环就会从程序计数器开始一条一条地执行指令。

如果不能保存和修改程序数据,那么执行指令就是毫无意义的。速度最快的数据存储器是处理器的寄存器组。一个寄存器是处理器内的一个存储单元,能够保存一个字大小的值(按照机器不同,一个字通常会是16,32或者64位)。寄存器内的值能在一个 CPU 周期内被快速地读写。

Expand Down
24 changes: 12 additions & 12 deletions content/AppendixB.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

###引导加载器(boot loader)

当 x86 PC 启动时,它执行的是一个叫 BIOS 的程序。BIOS 存放在非易失存储器中,BIOS 的作用是在启动时进行硬件的准备工作,接着把控制权交给操作系统。具体来说,BIOS 会把控制权交给从引导扇区(用于引导的磁盘的第一个512字节的数据区)加载的代码。引导扇区中包含引导加载器——负责内核加载到内存中。BIOS 会把引导扇区加载到内存 0x7c00 处,接着(通过设置寄存器 `%ip`)跳转至该地址。引导加载器开始执行后,处理器处于模拟 Intel 8088 处理器的模式下。而接下来的工作就是把处理器设置为现代的操作模式,并从磁盘中把 xv6 内核载入到内存中,然后将控制权交给内核。xv6 引导加载器包括两个源文件,一个由16位和32位汇编混合编写而成(`bootasm.S`;(8400)),另一个由 C 写成(`bootmain.c`;(8500))。
当 x86 PC 启动时,它执行的是一个叫 BIOS 的程序。BIOS 存放在非易失存储器中,BIOS 的作用是在启动时进行硬件的准备工作,接着把控制权交给操作系统。具体来说,BIOS 会把控制权交给从引导扇区(用于引导的磁盘的第一个512字节的数据区)加载的代码。引导扇区中包含引导加载器——负责内核加载到内存中。BIOS 会把引导扇区加载到内存 0x7c00 处,接着(通过设置寄存器 `%ip`)跳转至该地址。引导加载器开始执行后,处理器处于模拟 Intel 8088 处理器的模式下。而接下来的工作就是把处理器设置为现代的操作模式,并从磁盘中把 xv6 内核载入到内存中,然后将控制权交给内核。xv6 引导加载器包括两个源文件,一个由16位和32位汇编混合编写而成(`bootasm.S`;(8900)),另一个由 C 写成(`bootmain.c`;(9000))。

### 代码:汇编引导程序

引导加载器的第一条指令 `cli`8412)屏蔽处理器中断。硬件可以通过中断触发中断处理程序,从而调用操作系统的功能。BIOS 作为一个小型操作系统,为了初始化硬件设备,可能设置了自己的中断处理程序。但是现在 BIOS 已经没有了控制权,而是引导加载器正在运行,所以现在还允许中断不合理也不安全。当 xv6 准备好了后(详见第3章),它会重新允许中断。
引导加载器的第一条指令 `cli`8912)屏蔽处理器中断。硬件可以通过中断触发中断处理程序,从而调用操作系统的功能。BIOS 作为一个小型操作系统,为了初始化硬件设备,可能设置了自己的中断处理程序。但是现在 BIOS 已经没有了控制权,而是引导加载器正在运行,所以现在还允许中断不合理也不安全。当 xv6 准备好了后(详见第3章),它会重新允许中断。

现在处理器处在模拟 Intel 8088 的*实模式*下,有8个16位通用寄存器可用,但实际上处理器发送给内存的是20位的地址。这时,多出来的4位其实是由段寄存器`%cs, %ds, %es, %ss`提供的。当程序用到一个内存地址时,处理器会自动在该地址上加上某个16位段寄存器值的16倍。因此,内存引用中其实隐含地使用了段寄存器的值:取指会用到 `%cs`,读写数据会用到 `%ds`,读写栈会用到 `%ss`

Expand All @@ -16,31 +16,31 @@ xv6 假设 x86 指令在做内存操作时使用的是虚拟地址,但实际

引导加载器还没有允许分页硬件工作;它通过分段硬件把逻辑地址转化为线性地址,然后直接作为物理地址使用。xv6 会配置分段硬件,使之不对逻辑地址做任何改变,直接得到线性地址,所以线性地址和逻辑地址是相等的。由于历史原因我们用*虚拟地址*这个术语来指程序操作时用的地址。xv6 的虚拟地址等于 X86 的逻辑地址,同样也等于分段硬件映射的线性地址。等到开启了分页后,系统中值得关心的就只有从线性地址到物理地址的映射。

BIOS 完成工作后,`%ds, %es, %ss` 的值是未知的,所以在屏蔽中断后,引导加载器的第一个工作就是将 `%ax` 置零,然后把这个零值拷贝到三个段寄存器中(8415-8418)。
BIOS 完成工作后,`%ds, %es, %ss` 的值是未知的,所以在屏蔽中断后,引导加载器的第一个工作就是将 `%ax` 置零,然后把这个零值拷贝到三个段寄存器中(8915-8918)。

虚拟地址 *segment:offset* 可能产生21位物理地址,但 Intel 8088 只能向内存传递20位地址,所以它截断了地址的最高位:0xffff0 + 0xffff = 0x10ffef,但在8088上虚拟地址 0xffff:0xffff 则是引用物理地址 0x0ffef。早期的软件依赖硬件来忽略第21位地址位,所以当 Intel 研发出使用超过20位物理地址的处理器时,IBM 就想出了一个技巧来保证兼容性。那就是,如果键盘控制器输出端口的第2位是低位,则物理地址的第21位被清零;否则,第21位可以正常使用。引导加载器用 I/O 指令控制端口 0x64 和 0x60 上的键盘控制器,使其输出端口的第2位为高位,来使第21位地址正常工作(8436)。
虚拟地址 *segment:offset* 可能产生21位物理地址,但 Intel 8088 只有20位寻址空间,所以它截断了地址的最高位:例如 0xffff0 + 0xffff = 0x10ffef,但在8088上虚拟地址 0xffff:0xffff 却是引用物理地址 0x0ffef。早期的软件依赖硬件来忽略第21位地址位,所以当 Intel 研发出使用超过20位物理地址的处理器时,IBM 就想出了一个技巧来保证兼容性。那就是,如果键盘控制器输出端口的第2位是低位,则物理地址的第21位被清零;否则,第21位可以正常使用。引导加载器用 I/O 指令控制端口 0x64 和 0x60 上的键盘控制器,使其输出端口的第2位为高位,来使第21位地址正常工作(8920-8936)。

对于使用内存超过65536字节的程序而言,实模式的16位寄存器和段寄存器就显得非常困窘了,显然更不可能使用超过 1M 字节的内存。x86系列处理器在80286之后就有了*保护模式*。保护模式下可以使用更多位的地址,并且(80386之后)有了“32位”模式使得寄存器,虚拟地址和大多数的整型运算都从16位变成了32位。xv6 引导程序依次允许了保护模式和32位模式
对于使用内存超过65536字节内存的程序而言,实模式的16位寄存器和段寄存器就显得非常困窘了,显然更不可能使用超过 1M 字节的内存。x86系列处理器在80286之后就有了*保护模式*。保护模式下可以使用更多位的地址,并且(80386之后)有了“32位”模式使得寄存器,虚拟地址和大多数的整型运算都从16位变成了32位。xv6 引导程序依次开启了保护模式和32位模式

![figureB-2](../pic/fb-2.png)

在保护模式下,段寄存器保存着*段描述符表*的索引(见图表 B-2)。每一个表项都指定了一个基物理地址,最大虚拟地址(称为限制),以及该段的权限位。这些权限位在保护模式下起着保护作用,内核可以根据它们来保证一个程序只使用属于自己的内存。

xv6 几乎没有使用段;取而代之的是第2章讲述的分页。引导加载器将段描述符表 `gdt`8482-8485)中的每个段的基址都置零,并让所有段都有相同的内存限制(4G字节)。该表中有一个空指针表项,一个可执行代码的表项,一个数据的表项。代码段描述符的标志位中指示了代码只能在32位模式下执行(0660)。正是由于这样的设置,引导加载器在进入保护模式时,逻辑地址才会直接映射为物理地址。
xv6 几乎没有使用段;取而代之的是第2章讲述的分页。引导加载器将段描述符表 `gdt`8982-8985)中的每个段的基址都置零,并让所有段都有相同的内存限制(4G字节)。该表中有一个空指针表项,一个可执行代码的表项,一个数据的表项。代码段描述符的标志位中指示了代码只能在32位模式下执行(0660)。正是由于这样的设置,引导加载器在进入保护模式时,逻辑地址才会直接映射为物理地址。

引导加载器执行 `lgdt`8441)指令来把指向 `gdt` 的指针 `gdtdesc`8487-8489)加载到全局描述符表(GDT)寄存器中。
引导加载器执行 `lgdt`8941)指令来把指向 `gdt` 的指针 `gdtdesc`8987-8989)加载到全局描述符表(GDT)寄存器中。

加载完毕后,引导加载器将 `%cr0` 中的 `CR0_PE` 位置为1,从而开启保护模式。允许保护模式并不会马上改变处理器把逻辑地址翻译成物理地址的过程;只有当某个段寄存器加载了一个新的值,然后处理器通过这个值读取 GDT 的一项从而改变了内部的段设置。我们没法直接修改 `%cs`,所以使用了一个 `ljmp` 指令(8453)。跳转指令会接着在下一行(8456)执行,但这样做实际上将 `%cs` 指向了 `gdt` 中的一个代码描述符表项。该描述符描述了一个32位代码段,这样处理器就切换到了32位模式下。就这样,引导加载器让处理器从8088进化到80286,接着进化到了80386。
加载完毕后,引导加载器将 `%cr0` 中的 `CR0_PE` 位置为1,从而开启保护模式。允许保护模式并不会马上改变处理器把逻辑地址翻译成物理地址的过程;只有当某个段寄存器加载了一个新的值,然后处理器通过这个值读取 GDT 的一项从而改变了内部的段设置。我们没法直接修改 `%cs`,所以使用了一个 `ljmp` 指令(8953)。跳转指令会接着在下一行(8956)执行,但这样做实际上将 `%cs` 指向了 `gdt` 中的一个代码描述符表项。该描述符描述了一个32位代码段,这样处理器就切换到了32位模式下。就这样,引导加载器让处理器从8088进化到80286,接着进化到了80386。

在32位模式下,引导加载器首先用 `SEG_KDATA`8458-8461)初始化了数据段寄存器。逻辑地址现在是直接映射到物理地址的。运行 C 代码之前的最后一个步骤是在空闲内存中建立一个栈。内存 0xa0000 到 0x100000 属于设备区,而 xv6 内核则是放在 0x100000 处。引导加载器自己是在 0x7c00 到 0x7d00。本质上来讲,内存的其他任何部分都能用来存放栈。引导加载器选择了 0x7c00(在该文件中即 `$start`)作为栈顶;栈从此处向下增长,直到 0x0000,不断远离引导加载器代码。
在32位模式下,引导加载器首先用 `SEG_KDATA`8958-8961)初始化了数据段寄存器。逻辑地址现在是直接映射到物理地址的。运行 C 代码之前的最后一个步骤是在空闲内存中建立一个栈。内存 0xa0000 到 0x100000 属于设备区,而 xv6 内核则是放在 0x100000 处。引导加载器自己是在 0x7c00 到 0x7d00。本质上来讲,内存的其他任何部分都能用来存放栈。引导加载器选择了 0x7c00(在该文件中即 `$start`)作为栈顶;栈从此处向下增长,直到 0x0000,不断远离引导加载器代码。

最后加载器调用 C 函数 `bootmain`8468)。`bootmain` 的工作就是加载并运行内核。只有在出错时该函数才会返回,这时它会向端口 0x8a00(8470-8476)输出几个字。在真实硬件中,并没有设备连接到该端口,所以这段代码相当于什么也没有做。如果引导加载器是在 PC 模拟器上运行,那么端口 0x8a00 则会连接到模拟器并把控制权交还给模拟器本身。无论是否使用模拟器,这段代码接下来都会执行一个死循环(8477-8478)。而一个真正的引导加载器则应该会尝试输出一些调试信息。
最后加载器调用 C 函数 `bootmain`8968)。`bootmain` 的工作就是加载并运行内核。只有在出错时该函数才会返回,这时它会向端口 0x8a00(8970-8976)输出几个字。在真实硬件中,并没有设备连接到该端口,所以这段代码相当于什么也没有做。如果引导加载器是在 PC 模拟器上运行,那么端口 0x8a00 则会连接到模拟器并把控制权交还给模拟器本身。无论是否使用模拟器,这段代码接下来都会执行一个死循环(8977-8978)。而一个真正的引导加载器则应该会尝试输出一些调试信息。

### 代码:C 引导程序

引导加载器的 C 语言部分 `bootmain.c`8500)目的是在磁盘的第二个扇区开头找到内核程序。如我们在第2章所见,内核是 ELF 格式的二进制文件。为了读取 ELF 头,`bootmain` 载入 ELF 文件的前4096字节(8514),并将其拷贝到内存中 0x10000 处。
引导加载器的 C 语言部分 `bootmain.c`9000)目的是在磁盘的第二个扇区开头找到内核程序。如我们在第2章所见,内核是 ELF 格式的二进制文件。为了读取 ELF 头,`bootmain` 载入 ELF 文件的前4096字节(9014),并将其拷贝到内存中 0x10000 处。

下一步要通过 ELF 头检查这是否的确是一个 ELF 文件。`bootmain` 从磁盘中 ELF 头之后 `off` 字节处读取扇区的内容,并写到内存中地址 `paddr` 处。`bootmain` 调用 `readseg` 将数据从磁盘中载入(8538),并调用 `stosb` 将段的剩余部分置零(8540)。`stosb`(0492)使用 x86 指令 `rep stosb` 来初始化内存块中的每个字节。
下一步要通过 ELF 头检查这是否的确是一个 ELF 文件。`bootmain` 从磁盘中 ELF 头之后 `off` 字节处读取扇区的内容,并写到内存中地址 `paddr` 处。`bootmain` 调用 `readseg` 将数据从磁盘中载入(9038),并调用 `stosb` 将段的剩余部分置零(9040)。`stosb`(0492)使用 x86 指令 `rep stosb` 来初始化内存块中的每个字节。

在内核编译和链接后,我们应该能在虚拟地址 0x80100000 处找到它。因此,函数调用指令使用的地址都是 0xf01xxxxx 的形式;你可以在 `kernel.asm` 中找到类似的例子。这个地址是在 `kernel.ld` 中设置的。0x80100000 是一个比较高的地址,差不多处在32位地址空间的尾部;至于原因,我们在第2章中对此作出了详细解释。当然,实际的物理内存中可能并没有这么高的地址。一旦内核开始运行,它会开启分页硬件来将虚拟地址 0x80100000 映射到物理地址 0x00100000。引导程序运行到现在,分页机制尚未被开启。在`kernel.ld`中指明了内核的`paddr`是0x00100000,也就是说,引导加载器将内核拷贝到的低地址正是分页硬件最终会映射的物理地址。

Expand Down

0 comments on commit 473d910

Please sign in to comment.