从系统加电开始执行的几个文件顺序
BIOS---> bootsect.s ---> setup.s ---> System模块(head.s--->main.c)
下面我们就来依次分析这些文件。
1. bootsect.s
1 !
2 ! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
3 ! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
4 ! versions of linux
5 !
6 SYSSIZE = 0x3000 //要加载的系统模块的长度
7 !
8 ! bootsect.s (C) 1991 Linus Torvalds
9 !
10 ! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
11 ! iself out of the way to address 0x90000, and jumps there.
12 !
bootsect.s会被BIOS加载到内存地址的0x7c00处,然后它会把自己移动到内存绝对地址0x90000处,然后跳转到那开始执行。
这里涉及到三个问题:
1. 为什么要加载到0x7c00?
2. 怎么移动自身?
3.为什么要移动到0x90000处?
这里来回答一下这几个问题,加载到0x7c00我们可以看成一个规范,因为BIOS占用了内存低地址,所以引导文件就选择这个位置来加载,更具体的解释可以参见《
为什么主引导记录的内存地址是0x7C00?
》。第二个问题,移动自身很简单有相应的汇编代码可以实现(具体下面的源码中会给出),只要理解当前程序只是内存中的比特信息而已,移动它并不会影响当前程序的执行。第三个问题,0x90000=512K+0x10000.system会被加载到0x10000的位置,因为当时认为system代码长度不会超过512K,所以把bootsect移到0x90000位置处是安全的。
13 ! It then loads 'setup' directly after itself (0x90200), and the system
14 ! at 0x10000, using BIOS interrupts.
然后它会使用BIOS中断加载setup.s到它的后面,并把system加载到0x10000位置。
4.这里为什么要使用BIOS中断,而不是直接通过bootsect来加载?
因为bootsect要想加载文件需要文件系统的帮助,现在显然还不具备条件,其实这时采用的是BIOS的int 0x13号中断向量来处理的加载,前面的bootsect的加载使用的是int 0x19中断向量。前者与后者的不同之处在于前者可以在程序中指定需要加载的程序所在的扇区,而后者是由BIOS执行的,它只能固定的加载软盘第一扇区的代码。
16 ! NOTE! currently system is at most 8*65536 bytes long. This should be no
17 ! problem, even in the future. I want to keep it simple. This 512 kB
18 ! kernel size should be enough, especially as this doesn't contain the
19 ! buffer cache as in minix
20 !
21 ! The loader has been made as simple as possible, and continuos
22 ! read errors will result in a unbreakable loop. Reboot by hand. It
23 ! loads pretty fast by getting whole sectors at a time whenever possible.
继续往下看:
25 .globl begtext, begdata, begbss, endtext, enddata, endbss
26 .text
27 begtext:
28 .data
29 begdata:
30 .bss
31 begbss:
32 .text
第25行定义了全局标志符。26行定义了文本段起始地址;28行定义了数据段地址;30行定义了全局未初始化数据段。
链接器会把多个模块中的相同的段合并在一起。这里把三个段都定义在一个重叠地址范围中,因此实际上不分段。
5.为什么说这里把三个段都定义在一个重叠地址范围中?
这个问题下面回答了。
32 .text
33
34 SETUPLEN = 4 ! nr of setup-sectors
35 BOOTSEG = 0x07c0 ! original address of boot-sector
36 INITSEG = 0x9000 ! we move boot here - out of the way
37 SETUPSEG = 0x9020 ! setup starts here
38 SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
39 ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
定义了一些界定位置的量。
41 ! ROOT_DEV: 0x000 - same type of floppy as boot.
42 ! 0x301 - first partition on first drive etc
43 ROOT_DEV = 0x306
这里的ROOT_DEV是根文件系统设备号。
45 entry start
46 start:
47 mov ax,#BOOTSEG
48 mov ds,ax
49 mov ax,#INITSEG
50 mov es,ax
51 mov cx,#256
52 sub si,si
53 sub di,di
54 rep //重复执行下面的汇编指令
55 movw
这里就是实现把bootsect程序从0x7c00移到0x9000处。这里的rep执行执行过程就是从ds[di]移动内容到es[ei],总共执行ecx次。
56 jmpi go,INITSEG
跳转到0x9000:go处执行。这里的涉及到分段机制与寻址的关系。8086是16位CPU,而地址总线是20位,采用段基址加偏移的方式寻址。
57 go: mov ax,cs
58 mov ds,ax
59 mov es,ax
60 ! put stack at 0x9ff00.
61 mov ss,ax
62 mov sp,#0xFF00 ! arbitrary value >>512
这里可以看到数据段寄存器,附加段寄存器,堆栈段寄存器里面都指向了代码段,也就是没有分段,cs段寄存器则是在上面的跳转过程中(56行)自动设置的,指向0x90000。这对应了我们上面提出的一个疑问。最后给栈指针赋值。
6. 为什么注释中说sp的值只要远大于512就可以?sp在何处用到的?
因为刚开始后面大量内存都未使用,都可以用来做栈(?)。sp暂时没有用到,后面会涉及到。
继续:
64 ! load the setup-sectors directly after the bootblock.
65 ! Note that 'es' is already set up.
66
67 load_setup:
68 mov dx,#0x0000 ! drive 0, head 0
69 mov cx,#0x0002 ! sector 2, track 0
70 mov bx,#0x0200 ! address = 512, in INITSEG
71 mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
72 int 0x13 ! read it
73 jnc ok_load_setup ! ok - continue
74 mov dx,#0x0000
75 mov ax,#0x0000 ! reset the diskette
76 int 0x13
77 j load_setup
78
以 第0个驱动器,第0个磁头,第2个扇区,第0个磁道,内存地址512b,结束地址512+SETUPLEN 作为参数调用BIOS中断,读取相应setup的内容。这里我们又解答了上面的一个疑问,为什么要用BIOS中断,因为这时内核还没有初始化,也就无法响应中断,只能采用BIOS中断。这也是为什么我们不能把程序加载到内存地址从0开始的位置,因为那里还要为BIOS所使用。
7. 为什么setup从第二个扇区开始存放?
因为第一个扇区存放着bootsect.s ^_^.事实上第一个分区不止存放着引导文件,还有分区表等信息,具体可看下图。
图片摘自百度百科
继续分析,73行如果一切顺利,跳转到ok_load_setup
79 ok_load_setup:
80
81 ! Get disk drive parameters, specifically nr of sectors/track
82
83 mov dl,#0x00
84 mov ax,#0x0800 ! AH=8 is get drive parameters
85 int 0x13
86 mov ch,#0x00
87 seg cs
88 mov sectors,cx
89 mov ax,#INITSEG
90 mov es,ax
这里主要就是获取驱动器参数的。
这里有一条指令seg cs,我们来看一下它的解释:
seg cs
mov sectors,ax
mov ax,#INITSEG
要说明两点:
第一,seg cs 只影响到mov sectors,ax而不影响mov ax,#INITSEG
第二,如果以Masm语法写,seg cs和mov sectors,ax两句合起来等
价于mov cs:[sectors],ax,这里使用了间接寻址方式。
重复一下前面的解释,mov [sectors],ax表示将ax中的内容
存入ds:sectors内存单元,而mov cs:[sectors],ax强制以
cs作为段地址寄存器,因此是将ax的内容存入cs:sectors内存
单元,一般来说cs与ds的值是不同的,如果cs和ds的值一样,
那两条指令的运行结果会是一样的。(编译后的指令后者比前
者一般长一个字节,多了一个前缀。)
结论,seg cs只是表明紧跟它的下一条语句将使用段超越,因为在编
译后的代码中可以清楚的看出段超越本质上就是加了一个字节
的指令前缀,因此as86把它单独作为一条指令来写也是合理的。
以上解释来源于网络
我们继续回到上面的分析,上面的代码放到下面便于浏览:
87 seg cs
88 mov sectors,cx
89 mov ax,#INITSEG
90 mov es,ax
也就是把cx的值保存到cs:[sectors]中,然后把INITSEG也就是0x9000移到ax和es。
我们来看一下sector的定义
241 sectors:
242 .word 0
其实这就相当于C语言中的变量定义。这里把BIOS检测到的每磁道扇区数保存到sectors中。另外多说一点,这里为什么用seg cs呢,因为我们这里并没有分段。
我们继续分析:
92 ! Print some inane message
93
94 mov ah,#0x03 ! read cursor pos
95 xor bh,bh
96 int 0x10
97
98 mov cx,#24
99 mov bx,#0x0007 ! page 0, attribute 7 (normal)
100 mov bp,#msg1
101 mov ax,#0x1301 ! write string, move cursor
102 int 0x10
103
通过BIOS中断读取鼠标位置,并输出文字信息。
104 ! ok, we've written the message, now
105 ! we want to load the system (at 0x10000)
106
107 mov ax,#SYSSEG
108 mov es,ax ! segment of 0x010000
109 call read_it
110 call kill_motor
附加段寄存器指向0x010000,然后读取文件。我们来看一下读取操作:
151 read_it:
152 mov ax,es
153 test ax,#0x0fff
154 die: jne die ! es must be at64kB boundary
!对齐64KB边界
155 xor bx,bx ! bx is starting address within segment
156 rp_read:
157 mov ax,es
158 cmp ax,#ENDSEG ! have we loaded all yet?
159 jb ok1_read
160 ret
159行判断如果ax严格小于#ENDSEG,说明还没有读完,那么跳转到ok1_read,我们看一下:
161 ok1_read:
162 seg cs
163 mov ax,sectors //ax=扇区数
164 sub ax,sread //ax=扇区数-已读扇区,注意sread的初值定义在147 sread: .word 1+SETUPLEN ,可以看出system是从setup下一个扇区开始存放的
165 mov cx,ax //cx=还需要读的扇区数
166 shl cx,#9 //cx = 需要读的扇区数×512
167 add cx,bx //cx = 需要读的扇区数×512+偏移
168 jnc ok2_read //如果加上还要读的数据后不大于64K,跳转到ok2_read
169 je ok2_read //如果等于64K,也同样跳转
170 xor ax,ax //如果大于64K,那么ax=0
171 sub ax,bx //ax = ax - bx
172 shr ax,#9 //ax = ax/512
8. 168-169行的判断是出于什么原因?
回答这个问题需要说明一下这里寄存器的意义,166行的cx保存的是还需要读取的字节数,bx中保存的是目前es段寄存器中的偏移(不明白的话看下面的分析),读取工作是按照64KB为单位进行的,每次读满64KB之后就会更新es段寄存器,并重置bx为0.因此,如果剩余未读取的数据不足64K,直接调用ok2_read进行读取,如果大于64KB,这说明下面的处理过程出现了问题,那么就继续读取之前的扇区,重复整个过程。
我们看一下ok2_read:
173 ok2_read:
174 call read_track
看一下read_track操作:
198 read_track:
199 push ax
200 push bx
201 push cx
202 push dx //保存寄存器值
203 mov dx,track //dx=当前磁道
204 mov cx,sread //cx=当前磁道已读扇区数
205 inc cx //cx++,要开始读的扇区
206 mov ch,dl // ch=当前磁道号
207 mov dx,head //dx = 当前磁头号
208 mov dh,dl // dh = 磁头号
209 mov dl,#0 //dl = 驱动器号(0表示当前是A驱动器)
210 and dx,#0x0100 //磁头号不大于1
211 mov ah,#2 // ah = 功能号
212 int 0x13
213 jc bad_rt //出错就跳转到bad_rt
214 pop dx
215 pop cx
216 pop bx
217 pop ax
218 ret
如果读取出错,我们看一下处理过程:
219 bad_rt: mov ax,#0
220 mov dx,#0
221 int 0x13
222 pop dx
223 pop cx
224 pop bx
225 pop ax
226 jmp read_track
驱动器复位,再转到read_track处重新读取。
我们回到前面的分析,继续看ok2_read:
173 ok2_read:
174 call read_track
175 mov cx,ax
176 add ax,sread
177 seg cs
178 cmp ax,sectors
179 jne ok3_read
180 mov ax,#1
181 sub ax,head
182 jne ok4_read
183 inc track
可以看到179行如果条件满足会直接跳转到ok3_read,如果条件不满足,分两种情况,假设182行条件不满足,最终也会进入到ok3_read。区别是前者是当前磁道还有数据待读,跳转到ok_read3继续读取,后者是当前磁道已经读完,跳转到ok_read3读取下一个磁道。至于182行,是用来切换磁头的,读取过程是这样的,比如0号磁头磁道0现在读完了,那么下一次就从1号磁头开始读取,这时不需要增加磁道号,读完1号磁头对应的磁道0后,回来读取0号磁头的磁道1。
187 ok3_read:
188 mov sread,ax //sread = 当前磁道已经读取的扇区数
189 shl cx,#9 //cx = sread×512
190 add bx,cx //bx = sread×512+偏移
191 jnc rp_read //跳转到rp_read
192 mov ax,es
193 add ax,#0x1000
194 mov es,ax
195 xor bx,bx
196 jmp rp_read
这里可以看到191行与192行最终都会跳转到rp_read,区别是如果当前已经读满了64KB的数据,那么192行-194行会更新段寄存器es,为其增加64KB大大小,195行则重新置bx为0,也就是bx是用于计算是否读满64K的。现在来看一下rp_read:
156 rp_read:
157 mov ax,es
158 cmp ax,#ENDSEG ! have we loaded all yet?
159 jb ok1_read
160 ret
判断读取的内容是否已经达到ENDSEG,也就是说system是否已经加载完毕,如果没有读完就继续读,如果读完就返回。
整个加载system的过程比较复杂,我们在此加以总结:首先在ok1_read中进行判断即将读取的数据是否超过64KB,如果不超过就调用ok2_read继续读取,如果超过说明之前的操作出了问题,那么重新读取之前的扇区以期望下次可以恢复正常。在ok2_read中实际进行了读盘操作,读完之后需要判断下次读取的位置,首先看一下当前磁道是否读完,如果没有读完,就调用ok3_read继续读取;如果当前磁道读完了,需要判断当前的磁头,如果当前是磁头0,那么下次读取磁头1的同一个磁道;如果当前是磁头1那么下一次就需要读取磁头0的下一个磁道。接下来根据之前实际读盘返回的数据进行判断如果目前已经读取了64KB的数据,就更新es段寄存器,并复位bx寄存器;接下来继续调用rp_read,在这里面判断是不是已经加载完毕,如果没有,继续回到ok1_read重复整个过程。
回到最初的分析,假设我们已经把system读取到了0x10000开始处,并调用了kill_motor。
228 /*
229 * This procedure turns off the floppy drive motor, so
230 * that we enter the kernel in a known state, and
231 * don't have to worry about it later.
232 */
233 kill_motor:
234 push dx
235 mov dx,#0x3f2
236 mov al,#0
237 outb
238 pop dx
239 ret
我们继续向下看:
112 ! After that we check which root-device to use. If the device is
113 ! defined (!= 0), nothing is done and the given device is used.
114 ! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
115 ! on the number of sectors that the BIOS reports currently.
116
117 seg cs
118 mov ax,root_dev
119 cmp ax,#0
120 jne root_defined
121 seg cs
122 mov bx,sectors
123 mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
124 cmp bx,#15
125 je root_defined
126 mov ax,#0x021c ! /dev/PS0 - 1.44Mb
127 cmp bx,#18
128 je root_defined
root_dev的定义如下:
249 .org 508
250 root_dev:
251 .word ROOT_DEV
我们看一下root_defined:
131 root_defined:
132 seg cs
133 mov root_dev,ax
这里就是把引导设备的驱动号保存到root_dev中。
继续,
135 ! after that (everyting loaded), we jump to
136 ! the setup-routine loaded directly after
137 ! the bootblock:
138
139 jmpi 0,SETUPSEG
140
现在加载完毕,我们直接跳转到SETUPSEG(0x9020)处执行。到此这个程序就结束了。
244 msg1:
245 .byte 13,10
246 .ascii "Loading system ..."
247 .byte 13,10,13,10
248
249 .org 508
表示下面语句从508(0x1FC)开始,所以root_dev在启动扇区的第508字节开始的2个字节中。
250 root_dev:
251 .word ROOT_DEV //存放根文件系统所在的设备号(init/main.c中会用到)
下面是启动盘具有有效引导扇区的标志(可以看上面的图),仅供BIOS程序加载引导扇区时使用。它必须位于引导扇区最后两个字节。
252 boot_flag:
253 .word 0xAA55
254
255 .text
256 endtext:
257 .data
258 enddata:
259 .bss
260 endbss:
总结:BIOS加载了bootsect程序到内存0x7c00处,然后bootsect程序将自身移动到内存的0x90000处,并跳转到移动后的代码相应位置处执行,这里执行的内容就包括将setup加载到紧随其后的位置,而且还执行了加载system的操作。这些都是通过BIOS中断来完成的。最后bootsect跳转到setup处执行。
另外,通过读本程序,我们也可以对程序的本质有更深刻的理解,这里的定义变量的方式,以及标号的使用,以及段与偏移的结合使用,对我们理解寻址方式和程序连接也有很大的帮助。