• Linux-0.11 boot目录bootsect.s详解


    Linux-0.11 boot目录bootsect.s详解

    模块简介

    bootsect.s是磁盘启动的引导程序,其概括起来就是代码的搬运工,将代码搬到合适的位置。下图是对搬运过程的概括,可以有个印象,后面将详细讲解。

    启动中内存分布变化

    bootsect.s主要做了如下的三件事:

    • 搬运bootsect.s代码到0x9000:0x0000处
    • 加载setup.s代码到0x9000:0x200处
    • 加载system模块到0x1000:0x0000处

    过程详解

    step1:搬运bootsect.s代码到0x9000:0x0000

    下面是bootsect.s中开头1-50行。

    !
    ! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
    ! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
    ! versions of linux
    !
    SYSSIZE = 0x3000
    !
    !	bootsect.s		(C) 1991 Linus Torvalds
    !
    ! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
    ! iself out of the way to address 0x90000, and jumps there.
    !
    ! It then loads 'setup' directly after itself (0x90200), and the system
    ! at 0x10000, using BIOS interrupts. 
    !
    ! NOTE! currently system is at most 8*65536 bytes long. This should be no
    ! problem, even in the future. I want to keep it simple. This 512 kB
    ! kernel size should be enough, especially as this doesn't contain the
    ! buffer cache as in minix
    !
    ! The loader has been made as simple as possible, and continuos
    ! read errors will result in a unbreakable loop. Reboot by hand. It
    ! loads pretty fast by getting whole sectors at a time whenever possible.
    
    .globl begtext, begdata, begbss, endtext, enddata, endbss
    .text
    begtext:
    .data
    begdata:
    .bss
    begbss:
    .text
    
    SETUPLEN = 4				! nr of setup-sectors
    BOOTSEG  = 0x07c0			! original address of boot-sector
    INITSEG  = 0x9000			! we move boot here - out of the way
    SETUPSEG = 0x9020			! setup starts here
    SYSSEG   = 0x1000			! system loaded at 0x10000 (65536).
    ENDSEG   = SYSSEG + SYSSIZE		! where to stop loading
    
    ! ROOT_DEV:	0x000 - same type of floppy as boot.
    !		0x301 - first partition on first drive etc
    ROOT_DEV = 0x306
    
    entry _start
    _start:
    	mov	ax,#BOOTSEG
    	mov	ds,ax
    	mov	ax,#INITSEG
    	mov	es,ax
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    其中最关键的是下面这几行:

    	mov	ax,#BOOTSEG 
    	mov	ds,ax
    	mov	ax,#INITSEG
    	mov	es,ax
    
    • 1
    • 2
    • 3
    • 4

    这里首先将ax寄存器设置为0x07c0, 接着将ax寄存器的值拷贝给ds,即ds目前为0x07c0

    接下来将ax寄存器设置为0x9000, 接着将ax寄存器的值拷贝给es,即es目前为0x9000

    下面是bootsect.s中的51-55行:

    	mov	cx,#256 #设置移动计数值256字
    	sub	si,si   #源地址	ds:si = 0x07C0:0x0000
    	sub	di,di   #目标地址 es:di = 0x9000:0x0000
    	rep         #重复执行并递减cx的值
    	movw        #从内存[si]处移动cx个字到[di]处			
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里首先将cx的值设置为256

    接下来sub指令后跟了两个相同的si寄存器,这其实会将寄存器si设置为0。sub di,di同理将di设置为0。

    接下来使用rep前缀和movsw指令。

    根据Intel手册,movsw的作用是从DS:(E)SI拷贝一个字到ES:(E)DImovsw操作之后会对sisi进行递增或者递减,递增还是递减由EFLAGS寄存器中的方向位(DF: direction flag)来决定, DF=0,则进行递增, DF=1,则进行递减。

    因此rep movsw的实际作用是从ds:si拷贝256个字(512字节)到es:si处。

    接下来是bootsect.s的第56行,使用jmpi指令进行跳转。

    	jmpi	go,INITSEG
    
    • 1

    jmpi是段间跳转指令,jmpi的格式是jmpi 段内偏移, 段选择子

    这句指令使得程序跳转到0x9000偏移量为go处的代码执行。执行完之后cs寄存器的值将等于0x9000

    下面是bootsect.s的第57-62行。

    go:	mov	ax,cs  #将ds,es,ss都设置成移动后代码所在的段处(0x9000)
    	mov	ds,ax
    	mov	es,ax
    	mov	ss,ax
    	mov	sp,#0xFF00
    
    • 1
    • 2
    • 3
    • 4
    • 5

    cs寄存器的值为0x9000。接下来的操作就是将dsesss都赋值为0x9000。同时将sp设置为0xFF00

    step2:加载setup.s代码到0x9000:0x200

    接下来这一部分用于加载setup.s的代码到0x9000:0x200处。

    下面这里是bootsect.s的67-77行。

    load_setup:
    	mov	dx,#0x0000		! drive 0, head 0
    	mov	cx,#0x0002		! sector 2, track 0
    	mov	bx,#0x0200		! address = 512, in INITSEG
    	mov	ax,#0x0200+SETUPLEN	! service 2, nr of sectors
    	int	0x13			! read it
    	jnc	ok_load_setup		! ok - continue
    	mov	dx,#0x0000
    	mov	ax,#0x0000		! reset the diskette
    	int	0x13
    	j	load_setup
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这里利用了BIOS的0x13号中断,0x13中断和磁盘操作相关,这里使用了2号功能码。

    0x13中断的2号功能的各项参数含义如下:

    • AH = 02h
    • AL = 要读取多少扇区,非0的数值
    • CH = 柱面的低8位
    • CL = CL[5:0]表示起始扇区,CL[7:6]是柱面的高2位
    • DH = 磁头号
    • DL = 驱动器号
    • ES:BX = 数据缓冲区的位置

    返回值:

    • 当出错时CF被设置。
    • 成功操作时,CF被清楚。

    ax = 0x0204, 因此ah=0x02, al=0x04,代表这里进行的操作是都磁盘到内存,且要读取4个扇区。
    bx = 0x200, 因此从磁盘中读取的数据将拷贝到0x9000:0x200
    cx = 0x0002, cx[5:0] = 0x2,表示起始扇区为2,{cx[7:6], cx[15:8]} = 0x0, 代表柱面为0。
    dx = 0x0000dh = 0x0, 磁头号为0, dl = 0x0, 驱动器号为0。

    关于0x13中断的更多详细功能,可以参考这里

    如果读取成功则执行ok_load_setup。 这里使用的是jnc跳转指令,它会根据CF的状态决定是否跳转。

    如果不成功,则对驱动器进行复位,再次读取。复位操作仍然使用的是0x13中断,操作码为0。

    • AH = 00h
    • DL = 驱动器号

    接下来是bootsect.s的79-90行。

    ok_load_setup:
    
    ! Get disk drive parameters, specifically nr of sectors/track
    
    	mov	dl,#0x00
    	mov	ax,#0x0800		! AH=8 is get drive parameters
    	int	0x13
    	mov	ch,#0x00
    	seg cs
    	mov	sectors,cx
    	mov	ax,#INITSEG
    	mov	es,ax
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这里是使用0x13中断的8号功能码去获取一些磁盘驱动器的参数。

    入参:

    • ah:功能码,08h代表去获取驱动器的参数。
    • dl:为驱动器号。

    返回信息:

    • CF:如果出错则CF置为1,如果成功则CF=0。如果出错,ah=状态码
    • ah = 0, al = 0
    • bl:驱动器的类型(AT/PS2)
    • ch:最大柱面号的低8位
    • cl: cl[5:0]代表每磁道最大扇区数, cl[7:6]代表最大柱面号高2位
    • dh:最大磁头数
    • dl:驱动器数量
    • es:di:软驱磁盘参数表

    在调用完0x13中断获取完磁盘参数后,首先对ch进行置零,因为ch中存放的是最大柱面,而我们下面要去获取扇区数,因此避免其干扰。

    这下面使用mov sectors,cx将最大扇区数存在了sectors中。

    最后由于中断返回值修改了es,因此需要进行恢复。

    mov	ax,#INITSEG
    mov	es,ax
    
    • 1
    • 2

    下面是bootsect.s的第92-102行,主要使用BIOS中断0x10向终端中打印信息。

    ! Print some inane message
    
    	mov	ah,#0x03		! read cursor pos
    	xor	bh,bh
    	int	0x10
    	
    	mov	cx,#24
    	mov	bx,#0x0007		! page 0, attribute 7 (normal)
    	mov	bp,#msg1
    	mov	ax,#0x1301		! write string, move cursor
    	int	0x10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    0x10中断号有多个功能,具体含义如下:

    • 1.读光标位置,ah=0x03

      输入:

      • bh = 页号

      输出:

      • ch = 扫描开始线
      • cl = 扫描结束线
      • dh = 行号
      • dl = 列号
    • 2.打印字符串:ah=0x013

      输入:

      • al = 放置光标的方式和规定属性
      • es:bp 字符串位置
      • cx = 显示的字符串字符数
      • bh = 显示页面号
      • bl = 字符属性
      • dh = 行号
      • dl = 列号

    首先读取了目前光标所在的位置,存储在了dx中。

    	mov	ah,#0x03		! read cursor pos
    	xor	bh,bh
    	int	0x10
    
    • 1
    • 2
    • 3

    接着指定了要显示的字符串的长度为24, 页面号为0,字符属性为7,要显示的字符位置是0x9000:msg, 即\r\nLoading system ...\r\n\r\n,放置光标的方式和规定属性为0x1。

    	mov	cx,#24
    	mov	bx,#0x0007		! page 0, attribute 7 (normal)
    	mov	bp,#msg1
    	mov	ax,#0x1301		! write string, move cursor
    	int	0x10
    
    • 1
    • 2
    • 3
    • 4
    • 5

    step3:加载system模块到0x1000:0x0000

    接下来,要继续读system模块到内存中。

    下面是bootsect.s的104-110行:

    ! ok, we've written the message, now
    ! we want to load the system (at 0x10000)
    
    	mov	ax,#SYSSEG
    	mov	es,ax		! segment of 0x010000
    	call	read_it
    	call	kill_motor
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里实际会设置ax=0x1000, es = 0x1000,进而会调用read_it方法,稍后我们会详细理解read_it方法,这里我们先有个概念,read_it实际上作用就是将system模块存放在0x1000:0x0000处。

    read_it位于bootsect.s的第151行。

    首先看151-155行。

    read_it:
    	mov ax,es
    	test ax,#0x0fff
    die:	jne die			! es must be at 64kB boundary
    	xor bx,bx		! bx is starting address within segment
    
    • 1
    • 2
    • 3
    • 4
    • 5

    test指令执行AND运算,当AND运算的结果为零时,test设置零标志ZF=1

    而这里0x1000 & 0x0fff = 0x0000,因此ZF会被设置。jne跳转的条件是ZF == 0,因此不会进行跳转。这里的作用主要是用来检查es的值要在````64KB```的边界处。

    除此以外xor bx,bxbx的寄存器设置为0。

    接下来是第156-160行

    rp_read:
    	mov ax,es
    	cmp ax,#ENDSEG		! have we loaded all yet?
    	jb ok1_read
    	ret
    
    • 1
    • 2
    • 3
    • 4
    • 5

    rp_read的实际是逐磁道读取磁盘中system模块的过程。如下图所示共两个磁道,两个磁头,每磁道八个扇区,读取顺序如下所示,首先读取0磁头0磁道,然后读取1磁头0磁道,接着读取0磁头1磁道,最后读取1磁头1磁道。

    rp_read

    rp_read首先判断是否已经读入了所有的数据(system模块)。比较axENDSEG的值,如果不相等,则需要继续读取,于是跳转到ok1_read中执行。

    ok1_read位于161-172行:

    ok1_read:
    	seg cs
    	mov ax,sectors
    	sub ax,sread
    	mov cx,ax
    	shl cx,#9
    	add cx,bx
    	jnc ok2_read
    	je ok2_read
    	xor ax,ax
    	sub ax,bx
    	shr ax,#9
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ok1_read主要计算了当前磁道上还有多少扇区没有读取完,并将当期磁道上还剩下的扇区数存在了ax中, 将下一次读磁盘读到的字节数存到了cx中。

    下面这几句便是计算的过程:

    	mov ax,sectors
    	sub ax,sread          !当前磁道还有多少扇区没有读,下面调用read_track的时候会使用到该参数
    	mov cx,ax
    	shl cx,#9             !计算这一次读会读多少字节
    
    • 1
    • 2
    • 3
    • 4

    接下来进行判断,读到的数据是否超过了64KB。如果没有超过,则会跳转到ok2_read执行。

    	add cx,bx             !计算是否会超过64KB,64KB = 65536 = 1_00000000_00000000
    	jnc ok2_read
    	je ok2_read
    
    • 1
    • 2
    • 3

    ok2_read/ok3_read/ok4_read位于173-196行。

    ok2_read实际的作用就是将当前磁道上的所有扇区全部读完。更具体的,就是读取开始扇区cl和需读扇区数al的数据到es:bx开始处。

    ok2_read:
                              ! ax 先前已经设置
    	call read_track
    	mov cx,ax             !cx = ax,本次读取的扇区数
    	add ax,sread          !当前磁道上已经读取的扇区数
    	seg cs
    	cmp ax,sectors        !如果当前磁道上还有扇区未读,则跳转到ok3_read。
    	jne ok3_read
    	mov ax,#1             !
    	sub ax,head           !判断当前磁头号
    	jne ok4_read          !如果是0磁头,则再去读1磁头上的扇区数据。如果是1磁头
    	inc track             !否则读取下一个磁道
    ok4_read:
    	mov	%ax, head         !保存当前的磁头号
    	xor	%ax, %ax          !清除已读扇区数
    ok3_read:
    	mov	%ax, sread        !保存当前扇区已读扇区数
    	shl	$9, %cx
    	add	%cx, %bx
    	jnc	rp_read           !已经读取了64KB数据,调整当前段,为读取下一段数据做准备。
    	mov	%es, %ax
    	add	$0x1000, %ax      !刚开始是0x1000,读完64Kb之后,调整为0x2000,0x3000,最终到0x4000结束。
    	mov	%ax, %es
    	xor	%bx, %bx
    	jmp	rp_read
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    ok2_read的第一句话是调用了read_track方法:

    call read_track
    
    • 1

    read_track的作用是读取当前磁道上的扇面到es:bx处。

    read_track:
    	push ax            !保存ax,bx,cx,dx寄存器
    	push bx            
    	push cx            
    	push dx
    	mov dx,track       !获取当前的磁道号
    	mov cx,sread       !获取当前磁道上的已读扇区数
    	inc cx             !cl = 开始读的扇区号
    	mov ch,dl          !ch = 当前磁道号
    	mov dx,head        !dx = 当前磁头号, 目前磁头号还在dl中,后面会挪动到dh中。
    	mov dh,dl          !将磁头号从dl挪动到dh中
    	mov dl,#0          !dl = 驱动器号
    	and dx,#0x0100     !将dx与0x0100进行按位与,实际就是使得磁头号小于等于1。
    	mov ah,#2          !ah = 2,读磁盘扇区的功能号
    	int 0x13           !0x13号中断, 读取磁盘数据。
    	jc bad_rt          !如果出错,则跳转运行bad_rt
    	pop dx             !恢复dx,cx,bx,ax寄存器
    	pop cx
    	pop bx
    	pop ax
    	ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    read_track之后,会统计一些数据,看本磁道上的扇区是否全部读完,如果没有读完,则跳转到ok3_read进行再次读取。

    	mov cx,ax             !cx = ax,本次读取的扇区数
    	add ax,sread          !当前磁道上已经读取的扇区数
    	seg cs
    	cmp ax,sectors        !如果当前磁道上还有扇区未读,则跳转到ok3_read。
    	jne ok3_read
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果已经读完,则调整磁头和磁道,继续读取。这里指定了磁盘读取的顺序。

    	mov ax,#1             !
    	sub ax,head           !判断当前磁头号
    	jne ok4_read          !如果是0磁头,则再去读1磁头上的扇区数据。如果是1磁头
    	inc track             !否则读取下一个磁道
    ok4_read:
    	mov head,ax
    	xor ax,ax
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    需要分情况进行讨论:

    • 如果当前是0磁头0磁道,即head=0track=0ax = 1sub ax, head之后,ax = 1, 由于相减不等于0,因此ZF = 0jne会进行跳转, 于是head=1
    • 如果当前是1磁头0磁道,即head=1track=0ax = 0sub ax, head之后,ax = 0, 由于相减等于0,因此ZF = 1jne不会进行跳转, 于是head=0track=1
    • 如果当前是0磁头1磁道,即head=0track=1ax = 1sub ax, head之后,ax = 1, 由于相减不等于0,因此ZF = 0jne会进行跳转, 于是head=1

    总结起来读取顺序是首先读取0磁头0磁道,然后读取1磁头0磁道,接着读取0磁头1磁道,最后读取1磁头1磁道。

    读到这里应该对整个读取的过程有了一个概念,整个过程的流程如下所示:

    在这里插入图片描述

    看到这里,我们再回到调用read_it的地方, 我们看看当读取完system模块之后,还会做些什么操作。

    	mov	ax,#SYSSEG
    	mov	es,ax		! segment of 0x010000
    	call	read_it
    	call	kill_motor
    ! After that we check which root-device to use. If the device is
    ! defined (!= 0), nothing is done and the given device is used.
    ! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
    ! on the number of sectors that the BIOS reports currently.
    
    	seg cs
    	mov	ax,root_dev
    	cmp	ax,#0
    	jne	root_defined
    	seg cs
    	mov	bx,sectors
    	mov	ax,#0x0208		! /dev/ps0 - 1.2Mb
    	cmp	bx,#15
    	je	root_defined
    	mov	ax,#0x021c		! /dev/PS0 - 1.44Mb
    	cmp	bx,#18
    	je	root_defined
    undef_root:
    	jmp undef_root
    root_defined:
    	seg cs
    	mov	root_dev,ax
    
    ! after that (everyting loaded), we jump to
    ! the setup-routine loaded directly after
    ! the bootblock:
    
    	jmpi	0,SETUPSEG
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这里首先判断了root_dev是否进行了定义。如果定义了,则跳转到root_defined执行。

    0x0306

    	seg cs
    	mov	ax,root_dev
    	cmp	ax,#0
    	jne	root_defined
    
    • 1
    • 2
    • 3
    • 4

    这里需要补充一下Linux-0.11对于设备号的定义:

    设备号 = 主设备号 ∗ 256 + 次设备号 设备号 = 主设备号 * 256 + 次设备号 设备号=主设备号256+次设备号

    主设备号: 1-内存,2-磁盘,3-硬盘, 4-ttyx,5-tty,6-并行口,7非命名管道

    主设备号设备名含义
    0x300/dev/hd0代表第1块硬盘
    0x301/dev/hd1代表第1块硬盘的第1个分区
    0x302/dev/hd2代表第1块硬盘的第2个分区
    0x303/dev/hd3代表第1块硬盘的第3个分区
    0x304/dev/hd4代表第1块硬盘的第4个分区
    0x305/dev/hd5代表第2块硬盘
    0x306/dev/hd6代表第2块硬盘的第1个分区
    0x307/dev/hd7代表第2块硬盘的第2个分区
    0x308/dev/hd8代表第2块硬盘的第3个分区
    0x309/dev/hd9代表第2块硬盘的第4个分区

    在Linux-0.11中,这里的ROOT_DEV定义为0x306,因为当年linus是在第2块硬盘上的安装的文件系统。在编译内核时,可以根据自己的环境修改该参数。

    假设没有定义ROOT_DEV,就需要根据BIOS的每磁道扇区数来决定是使用/dev/PS0(0x0208)还是/dev/at0(0x021c)。

    	seg cs
    	mov	bx,sectors
    	mov	ax,#0x0208		! /dev/ps0 - 1.2Mb
    	cmp	bx,#15
    	je	root_defined
    	mov	ax,#0x021c		! /dev/PS0 - 1.44Mb
    	cmp	bx,#18
    	je	root_defined
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    程序的最后,因为所有的需要加载的内容都加载完了,于是执行jmpi 0,SETUPSEG跳转到了setup.s中进行执行。

    文章的最后,我们通过一张图回顾一下bootsect.s所做的一些事情:

    在这里插入图片描述

    文中如有表达不正确之处,欢迎大家与我交流。


    参考文章

    https://github.com/Wangzhike/HIT-Linux-0.11/blob/master/1-boot/OS-booting.md

  • 相关阅读:
    GS5812B 封装SOT23-6 一款用于无线充电2A , 21V 600KHZ同步降压IC
    Arduino UNO通过PCF8574串行IIC接口驱动LCD1602/LCD2004液晶屏
    满帮再现Non-GAAP盈利,恢复注册后预期增长更积极?
    IDEA的模板(Templates)
    【经验】解决重置 Windows 10 时报错:“无法找到介质” 的错误
    c#单例模式
    Java 面试八股文 —— SSM 框架常见面试题
    logistic 文本分类
    C 语言 while 和 do...while 循环
    实现有序的UUID
  • 原文地址:https://blog.csdn.net/qq_31442743/article/details/130907425