系统引导源码分析bootsect.s
阅读原文时间:2021年04月20日阅读:1

从系统加电开始执行的几个文件顺序

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处执行。

另外,通过读本程序,我们也可以对程序的本质有更深刻的理解,这里的定义变量的方式,以及标号的使用,以及段与偏移的结合使用,对我们理解寻址方式和程序连接也有很大的帮助。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器