Release version 0.5
[thunix.git] / doc / Chapter2-boot
blob72e60763991c2b6cf269547eff26db660bfba7a5
1 Chapter 2   操作系统的引导
3 这一章节讲解了操作系统的引导部分。 本章一开始描述了开机键一按之后的内幕, 如何加载到正确的内核。
5 2.1 计算机的启动
6 不知大家有没有想过, 我们一按计算机开关, 计算机内部都做了些什么? 当然, 结果可想而知, 如果正常, 我们将进入操作系统(linux或是windows). 但计算机是如何找到操作系统的, 特别是当我们安装了多操作系统的时候, 如在第一个分区安装windows, 在第二个分区安装linux. 要回答这个问题, 我们就得从开机时计算机都做了些什么说起。
8 当开机键一按时, 一种特殊硬件会触发一个RESET信号, 当CPU一收到该信号时, 某些寄存器就被设为些固定的值, 此时就位于内存0xfffffff0处的代码就会被执行, 这就是大家常说的BIOS代码所在的位置。 这时就完成了控制权的第一次交接, 此时BIOS就会执行一系统的硬件检测, 以使那些必需的设备能正常工作, 这就是所谓的POST(power-on self-test), 接着就是初始化那些设备, 之后便是寻找可启动的设备(第一个扇区的末尾两字节是0x55AA), 若找着了, 它就将完成BIOS所要做的最后一件事情,那就是读取该设备的第一个扇区(512B, 光盘是2K)到内存地址0x7c00, 并跳转到该地址继续执行。 这便完成了第二次控制权的交接。
10 理论上, 到此, 操作系统已获得了绝对的控制权。 然而, 这仅当我们从软盘启动时才是这么回事。对于其它设备, 如硬盘等, 就相对复杂了, 还有个第三次控制权的交接。
12 这么说, 磁盘设备的第一个扇区叫做引导扇区(bootsector),存储着引导操作系统的代码。对于软盘来说, 仅有一个这样的扇区, 所以, 它的内容也就很简单, 做些初始化, 把内核加载到适当的内存地址,并跳转到那执行, 之后操作系统就起来了。 但对于硬盘来说, 情况相对复杂, 这里仅作简述, 更多的内容可以参考些硬盘技术相当的文档。 情况复杂在哪呢, 硬盘是有分区的, 而软盘是没有的。 也就是说一个硬盘里面可以装载很多个系统, 但就仅仅512字节的一个扇区是不足以把所有的系统都加载进内存的。 所以, 最终的情况就是这样, 硬盘的第一个扇区称为主引导扇区(Master Boot Record), 简称MBR, 而每个分区的第一个扇区才是刚才所说的引导扇区。 所以, 对于硬盘来说, 当BIOS把MBR读入内存时, 那段MBR的就会把第一个可引导分区的引导扇区读了内存, 然后由那刚读了的512字节进行操作系统的初始化与加载。 那MBR如何能找着正确的分区并启动该分区的操作系统呢? 那是因为在MBR的512字节中, 有64字节记载着4个主分区的分区情况, 如这个扇区是否能引导(第一个扇区有无引导标志--0x80), 该分区的从第几个扇区开始, 第几个扇区结束等等, 所以, MBR就会读取第一个可引导分区的引导扇区, 读取并跳转到那执行完成操作系统的加载。 或许你还会回, 这并没有解决多个操作系统的选择与引导呀? 是的, 这就是那些bootloader做的事情了, 现最常用的有grub[GRUB]和syslinux[Syslinux]等等。 当然, 这里也不再作描述, 不过, 关于其原理与实现, 可参考其官网, 并且它们都是开源软件。
15 2.2 引导扇区(bootsetcor)
16 鉴于硬盘的复杂度, 我们这里仅以软盘作为例子。 引导扇区的任务很明确, 尽可能的做些初始化操作, 尽可能的把内核加载进内存并跳转到那执行。 然后, 我之后以会说‘尽可能的’, 那是因为, 仅小小的512字节 很难完成这如此多的操作, 其实, 并没有512字节, 最起码得扣除那两个引导标志, 更者, 对于硬盘来说仅有440不到的字节。  所以, 通过来说, bootsector并不会把内核加载进内存, 而是把一些初始化的代码加进内存, 而由这些代码再把进行更多的初始化操作与内核加载, 这相对更容易些, 因为此时没有512的限制了。 Thunix也是这么做的。
18 2.3 最微型的操作系统的实现  
19 前面讲了这么多关于理论的东西,是时候写些代码了。 就如前面所讲, 我们仅需要在软盘的第一个扇区写上一些简短的代码,并保证第一个扇区的最后俩字节来0x55AA就行了。 下面就是全部的代码, 要理解这些代码, 你得需要些简单的x86汇编语言知识, 并了解些AT&T格式的汇编(若不知道, 也不要紧, 你只需要把操作数的顺序换一下就可以了, 如Intel格式 mov ax, bx 对应的AT&T格式 movw %bx, %ax, 相信你能看出区别来), 以及一些常见的vm使用, 如bochs, qemu, 当然vmware也行。 这里我选择bochs,因为它很方便, 也很轻轻便,当然, bochs最麻烦的就是它得有个配置文件 , 关于配置文件, 可参考些bochs文档。 最后, 我使的环境是32位linux(Fedora, SuSE..., 都可以), 原因很简单, linux简直就是开发者的天堂。
21 --------------------------CODE--------------------------------
22 #       
23 # Print A char 'a' 
25         movb $0x0e, %ah
26         movb $0x61, %al   # print 'a', ASCII 0x61
27         movb $0x0f, %bh
28         movb $0x00, %bl
29         int $0x10         # BIOS int, it will echo a char at the screen
31 hang:                    
32         jmp hang          # The OS stops here
34 .org 0x1fe, 0x90          # Fill the blank with 0x90, the NOP instruction
35 .word 0xaa55              # bootsector flag
36 ----------------------------------------------------------
38 This is it. 赶紧 make && make bochs 吧(之后便会附上Makefile)。 我不知道谁还能找到比这更简单的! 功能很简单, 就是在通过调用0x10 BIOS中断[BISO INT]在屏幕上打印一个字符 'a'. 但, 它没有依赖于任何一个平台, 没有依赖于任何一个库函数, 所以,从某种意义上来说, 这是个操作系统, 只不过功能很简单, 什么事都不做, 只打印一个字符。
40 ------------附上 Makefile -----------------
41 all: boot.img
43 boot.o: boot.s
44         as -o boot.o boot.s
46 boot.img: boot.o
47         ld --oformat binary -N -e start -Ttext 0x7c00 -o boot.img boot.o
50 bochs:
51         bochs -qf bochsrc
53 clean:
54         rm -rf *.o *.img *.txt
55 ----------------------------------------
56 简单来说, Makefile定义了些编译规则, 这会告诉编译器对什么样的文件该进行什么样的操作, 什么文件需要编译, 而哪些又不需要编译, 使得再大的一个工程都可以通过敲击make这么一命令完成编译, 如linux内核的编译仅需make config && make, 之后便可以得到可启动的linux内核image。
58 如上面的Makefile, all: boot.img表示我们最终要生成的目标是boot.img(不过,注意的是all,不是Makefile里的关键字), 
59 boot.o: boot.s
60         as -o boot.o boot.s
61 表示对boot.s编译, 生成结果为boot.o, 注意, 在as(也即命令之前是一个TAB, 不能用8个空格代替)。 冒号之前表示目标, 之后的表示用于生成这些目标的源, 或叫依赖, 下面的便是用于生成目标的命令。 有些目标 可以不使用任何源依, 如bochs, 表示这可以通过 make bochs 命令运行, 如make clean将会做些清除工作, 等等。 关于Makefile就简单介绍到这, 可查看make[make]相关文档获得更详细的内容。
64 2.4 实用的bootsector引导代码
65 现在对上面所讲的做个小结, 就如以下流程图所表示:
66 ---------------------------------
67         开机, CPU收到RESET信号, 跳到BIOS处
68                  |
69                  |
70 ---------------------------------
71         |       POST
72         |        |
73         |    设备初始化
74         |        |           没有
75         |    引导设备检测   ------> 显示没有可引导设备, 并死机
76 BIOS    |        |
77         |      有|
78         |        |
79         | 读取引导扇区到内存地址0x7c00, 并跳转到此处执行
80 ---------------------------------
81         |        |
82 boot    |        |
83 sector  |初始化, 如初始化cs, ds, ss, sp等寄存器
84         |载入内核至指定的内存, 并跳转到此处执行
85         |        | 
86 ---------------------------------
87         |系统已经跑起来了
88 ---------------------------------
89                 
90 2.4.1 初始化
91 对于一个简单的bootsctor来说, 初始化很简单, 只包括些主要段寄存器的设置, 堆栈的建立。 简要代码如下:
92         movw $0x00, %ax
93         movw %ax,   %ds
94         movw %ax,   %ss
95         movw $0x7c00, %sp                       
96 当然, 也可以通过BIOS 0x10 0号中断初始化输出屏模式设置, 详情请参考BISO_int_0x10[INT 0x10]
98 2.4.2 加载内核
99 对于一个bootsector来说, 最主要的使命就是把内核加载到内存。 对于磁盘的读取, 又得依靠于BIOS的读扇区中断,
101 INT 13h[INT 0x13] AH=02h: 读扇区中断
103 参数 :
104 AH      02h
105 AL      要断的扇区数 
106 CH      磁道(Track)
107 CL      扇区号(Sector)
108 DH      磁头号(Head)
109 DL      驱动器标码(Drive), 0x0 为软盘, 0x80为硬盘
110 ES:BX   buffer地址        
112 结果 :
113 CF      置位表示出错, 反之表示成功
114 AH      返回码
115 AL      实际扇区所读数
117 所以, 实际的代码可能像这样子(只含读扇区代码):
118                 ## in:  ax:     LBA address, starts from 0
119                 ## es:bx address for reading sector
120 read_sect:
121                 pushw   %ax
122                 pushw   %cx
123                 pushw   %dx
124                 pushw   %bx
126                 movw    %si,    %ax
127                 xorw    %dx,    %dx
128                 movw    $18,    %bx     # 18 sectors per track
129                                         # for floppy disk
130                 divw    %bx
131                 incw    %dx
132                 movb    %dl,    %cl     # cl=sector number
133                 xorw    %dx,    %dx
134                 movw    $2,     %bx     # 2 headers per track
135                                         # for floppy disk
136                 divw    %bx
138                 movb    %dl,    %dh     # head
139                 xorb    %dl,    %dl     # driver
140                 movb    %al,    %ch     # cylinder
141                 popw    %bx             # save to es:bx
142 rp_read:
143                 movb    $0x1,   %al     # read 1 sector
144                 movb    $0x2,   %ah
145                 int     $0x13
146                 jc      rp_read
147                 popw    %dx
148                 popw    %cx
149                 popw    %ax
150                 ret
151 这里有两个要注意的地方: 第一, 很明显, 这里对某些寄存器值做了保护操作, 值得注意的就是出栈时的顺序与进栈时相反。 第二, 我们这里传给ax的值是以LBA(Logical Block Address)方式取址, 而不是CHS模式, 所以在调用int 0x13之前, 必需做个转换。 关于LBA以及LBA与CHS的转换可参考该文[LBA to CHS]。至于你想把你的内核加载到内存哪个地址, 你仅需把该值设给ES:BX即可, 值得注意的就是小心把BIOS给覆盖掉了, BIOS位于从0开始的第1K字节。
153 2.5 跳转到内核
154 假如上一步已经把内核加载到了内存地址0x1000, 那么下面的指令就会跳转到那, 并执行刚载入的内核。
155         ljmp $0x00, $0x1000
157 2.6 小结
158 至此, 一个功能完整的bootsector就算完成了, 它可以加载相应的内核到你所想要的任何内存地址(理论上是的), 也可以跳转到那执行内核。