bootsect.S及setup.S两个档案

http://doc.linuxpk.com/618.html

本文的目的,在将linux kernel的boot部份做一个介绍,因为笔者觉得很少有这样的

  文章来介绍一个操作系统最最开始的一步----把kernel本身载入至内存中,同时进行一些

  机器相关(machine dependent)的初始化工作,由于linux刚好使用的是大家最熟悉的386,

  486系列PC,所以在说明其程序流程时,也刚好可以对其相关的PC硬体架构做探讨,可以

  说是一举两得。不过,我必须假设读者对于汇编语言及PC最基础的架构,如寄存器,分段,

  分页,中断服务等有大概的认识。

  读者可在linux source code的/boot子目录下找到几个以.S作为副档名的组合语言档,

  本文要说明的即是其中的bootsect.S及setup.S两个档案,及尽量简单地说明其所牵涉的

  相关硬件部份。

  bootsect.S

  这个程序是linux kernel的第一个程序,包括了linux自己的bootstrap程序,但是

  在说明这个程序前,必须先说明一般IBM PC开机时的动作(此处的开机是指"打开PC的电源"):

  一般PC在电源打开时,是由内存中地址FFFF:0000开始执行(这个地址一定在ROMBIOS

  中,ROMBIOS一般是在FE000h到FFFFFh中),而此处的内容则是一个jump指令,jump到另

  一个位于ROMBIOS中的位置,开始执行一系列的动作,包括了检查RAM,keyboard,显示

  器,软硬磁盘等等,这些动作是由系统测试码(system test code)来执行的,随着制作

  BIOS厂商的不同而会有些许差异,但都是大同小异,读者可自行观察自家机器开机时,

  屏幕上所显示的检查讯息。

  紧接着系统测试码之后,控制权会转移给ROM中的启动程序(ROM bootstrap routine),

  这个程序会将磁盘上的零道零扇区读入内存中(这就是一般所谓的bootsect,如果你曾

  接触过电脑病毒,就大概听过它的大名),至于被读到内存的哪里呢?----绝对位置07C0

  :0000(即07C00h处),这是IBM系列PC的特性。而位在linux开机磁盘的bootsect上的正

  是linux的bootsect程序,也就是说,bootsect是第一个被读入内存中并执行的程序。

  现在,我们可以开始来看看到底bootsect做了什么。

  第一步

  首先,bootsect将它"自己"从被ROMBIOS载入的绝对地址0x7C00处搬到0x90000处,

  然后利用一个jmpi(jumpindirectly)的指令,跳到新位置的jmpi的下一行去执行,关键

  的汇编代码如下:

  .

  (搬移bootsect本身)

  .

  .

  jmpi go,INITSEG

  go:

  .

  .

  .

  表示将跳到CS为0x9000,IP为offset"go"的位置(CS:IP=0x9000:offsetgo),其中

  INITSEG=0x9000定义于程序开头的部分,而go这个label则恰好是下一行指令所在的位置。

  第二步

  接着,将其它segment registers包括DS,ES,SS都指向0x9000这个位置,与CS看齐。

  另外将SP及DX指向一任意位移地址(offset),这个地址等一下会用来存放磁盘参数表

  (disk parameter table)。

  提到磁盘参数表,就必须提到BIOS中断1Eh。先简单地介绍一下BIOS的中断服务:

  80x86将内存最低的256*4bytes保留给256个中断向量(每个interrupt vector大小为4bytes,

  所以一共有256*4=1024bytes),而其中的第1Eh个向量指向"磁盘参数表",这个表会告诉

  电脑如何去读取磁盘机,而我们所要做的事是搬移磁盘参数表到刚才所设定的任意地址。

  接着,改变搬移来的参数表的参数,以符合我们的需要。再将中断向量1Eh指向我们

  所修改过的磁盘参数表,然后呼叫BIOSinterrupt的int13h(function0,即AH=0)重置磁

  盘控制卡及磁盘驱动器,之后磁盘机就会照我们的意思动作了。如果你曾trace过DOS的

  kernel,你会发现,上述的动作在DOS中也有类似的对应流程。

  现在让我们来看看关键的程序码:.

  .

  .

  push #0

  pop fs

  mov bx,#0x78

  .

  (使GS:SI=FS:BX,指向磁盘参数表,

  再将GS:SI所指地址的内容搬移6个

  word至ES:DI所指的地址)

  .

  .

  此段程序是将FS:BX调整成0000:0078,接着再将GS:SI的内容设成与FS:BX相同,此

  处0x78h即为int1Eh的起始位置(7*16+8=120,(1*16+14)*4=120)。调整ES:DI为刚才所设

  定的任意地址,从GS:SI搬移6个word(即12byte)到ES:DI所指的位置,显然磁盘参数表的

  长度就是6个word,(不过事实上,磁盘参数表的确实长度是11个byte)。关于磁盘参数表,

  有兴趣的读者可自行参阅讲述BIOSinterruptservices的技术手册,会有详细的说明。

  读者可以用debug自行观察自家机器上dos的磁盘参数表的起始位置(即int1Eh的内容)。

  以下是笔者机器的情形(笔者使用的操作系统是msdos6.2):

  C:>debug

  -d0000:0000

  0000:0000

  8A101601F4067000-1600CB04F4067000......p.......p.

  0000:0010

  F40670000301790E-43EB00F0EBEA00F0..p...y.C.......

  0000:0020

  04108E340C118E34-5700CB046F00CB04...4...4W...o...

  0000:0030

  8700CB0408079433-B700CB04F4067000.......3......p.

  0000:0040

  0C01790E4DF800F0-41F800F0BA165F06..y.M...A....._.

  0000:0050

  39E700F01B01790E-70118E341201790E9.....y.p..4..y.

  0000:0060

  00E000F085175F06-6EFE00F0EE067000......_.n.....p.

  0000:0070

  53FF00F0A4F000F0-220500003E4600C0S......."...>F..

  ^^^^^^^^

  由上图中可知,在DOS中磁盘参数表的起始位置(int1Eh的内容)为0000:0522。接着观

  察dos中位置0000:0522开始的11个byte,也就是磁盘参数表的内容

  C:>debug

  -d0000:0520l10

  0000:0520

  4D53DF022502121B-FF54F60F08000000MS..%....T......

  ^^^^^^^^^^^^^^^^^^^^^^

  此11byte即为磁盘参数表的内容(分别是byte00h到0Ah)

  在程序中我们所更动的是第五个byte(byte04h),改为18h(在上图例子中为12h),这

  个byte的功能是定义磁轨上一个磁区的资料笔数。关键的程序码如下:

  .

  movb 4(di),*18

  .

  第三步

  接着利用BIOS中断服务int13h的第0号功能,重置磁盘控制器,使得刚才的设定发挥

  功能。

  .

  .

  xor ah,ah

  xor dl,dl

  int 0x13

  .

  .

  第四步

  完成重置磁盘控制器之后,bootsect就从磁盘上读入紧邻着bootsect的setup程序,

  也就是以后将会介绍的setup.S,此读入动作是利用BIOS中断服务int13h的第2号功能。

  setup的image将会读入至程序所指定的内存绝对地址0x90200处,也就是在内存中紧邻着

  bootsect所在的位置。待setup的image读入内存后,利用BIOS中断服务int13h的第8号功

  能读取目前磁盘机的参数。

  第五步

  再来,就要读入真正linux的kernel了,也就是你可以在linux的根目录下看到的

  vmlinuz。在读入前,将会先呼叫BIOS中断服务int10h的第3号功能,读取游标位置,之

  后再呼叫BIOS中断服务int10h的第13h号功能,在萤幕上输出字符串"Loading",这个字

  符串在boot linux时都会首先被看到,相信大家应该觉得很眼熟吧。

  linux的kernel将会被读入至内存绝对地址0x10000处,关键的程序码如下:

  .

  .

  mov ax,#SYSSEG

  mov es,ax

  call read_it

  call kill_motor

  .

  .

  其中SYSSEG于程序开头时定义为0x1000,先将ES内容设为0x1000,接着在read_it这

  个子程序,便以ES为目的地的节地址,将kernel读入内存中,至于read_it子程序的详细内

  容笔者并不想一一介绍,不过聪明的读者们应该已经猜到,read_it一定又利用了BIOS

  int13h与磁盘有关的I/O中断服务了。

  至于kill_motor子程序,它的功能在于停止软盘机的马达(各位聪明的读者会不会觉

  得这个子程序的名称取得颇为传神呢?),其程序码如下:

  .

  .

  kill_motor:

  push dx

  mov dx,#0x3f2

  xor al,al

  outb

  pop dx

  ret

  .

  .

  首先利用DX指定要输出的port,而03f2这个port则是代表了软盘控制器(floppy

  disk controller)的所在,再利用outb将资料送出,而我们送出的资料,当然就是归零过的

  AL了。如此一来,软盘的马达就停止了。

  第六步

  接下来做的事是检查root device,之后就仿照一开始的方法,利用indirect jump跳

  到刚刚已读入的setup部份,程序码如下:

  .

  .

  jmpi 0,SETUPSEG

  其中SETUPSEG已在先前定义为0x9020,所以CS:IP会设定为9020:0000,即跳到绝对

  地址为0x90200,也就是setup的起点,而bootsect也大功告成了。

  到此为止,内存的内容应该如下图所示:

  比较

  把大家所熟知的msdos与linux的开机部份做个粗浅的比较,msdos由位于磁盘上

  bootsect的boot程序负责把io.sys载入内存中,而io.sys则负有把dos的kernel--

  msdos.sys载入内存的重大责任。而linux则是由位于bootsect的bootsect程序负责

  把setup及linux的kernel载入内存中,再将控制权交给setup。

  至于setup.S,就留到下一次再来讨论了。


补充:

-----------------------------------------------------------------

#include /* for CONFIG_ROOT_RDONLY */
#include
SETUPSECS = 4 /* default nr of setup-sectors */
BOOTSEG = 0x07C0 /* original address of boot-sector */
INITSEG = DEF_INITSEG /* we move boot here - out of the way */
SETUPSEG = DEF_SETUPSEG /* setup starts here */
SYSSEG = DEF_SYSSEG /* system loaded at 0x10000 (65536) */
SYSSIZE = DEF_SYSSIZE /* system size: of 16-byte clicks */
ROOT_DEV = 0 /* ROOT_DEV is now written by "build" */
SWAP_DEV = 0 /* SWAP_DEV is now written by "build" */
#ifndef SVGA_MODE
#define SVGA_MODE ASK_VGA
#endif
#ifndef RAMDISK
#define RAMDISK 0
#endif
#ifndef CONFIG_ROOT_RDONLY
#define CONFIG_ROOT_RDONLY 1
#endif
.code16
.text
.global _start
_start:
/* 1: 把bootsect从0x7c00->0x90000,共512Bytes */
movw $BOOTSEG, %ax
movw %ax, %ds
movw $INITSEG, %ax
movw %ax, %es
movw $256, %cx
subw %si, %si /* si = 0 */
subw %di, %di /* di = 0 */
cld /* 清方向标志,把标志(flags)寄存器的DF=0,地址指针si、di增加;如是std则把DF=1,地址指针减小 */
rep /* if cx != 0, continue */
movsw /* ds:si -> es:di; si = si + 2, di = di + 2 */
ljmp $INITSEG, $go /* ljmp $SECTION, $OFFSET */
/* 2: 设置堆栈指针 sp = 0x4000 - 12,ss = 0x9000;栈顶:0x94000 - 12 */
go:
movw $0x4000-12, %di /* (bootsect长度+setup长度+堆栈长度) movw %ax, %ds /* ax and es already contain INITSEG */
movw %ax, %ss
movw %di, %sp /* put stack at INITSEG:0x4000-12 */
/* 3:磁盘参数表的中断向量(1Eh)由 0x0000:0x0078->0x9000:0x4000-12;1Eh在0x78开始的4Bytes */
movw %cx, %fs /* set fs to 0; cs, ds, es, ss are all 0x9000 */
movw $0x78, %bx /* fs:bx is parameter table address */
pushw %ds
ldsw %fs:(%bx), %si /* 把地址fs:bx(0x78)的4Bytes内容给ds:si,4Byte内容即为指向磁盘参数表的指针 ?????*/
movb $6, %cl /* copy 12 bytes */
pushw %di /* di = 0x4000-12 */
rep /* don't need cld -> done above */
movsw
popw %di
popw %ds
movb $36, 0x4(%di) /* 设置磁盘扇区数为36; 重置磁盘参数表的原因就是bios可能不支持多个扇区读操作 */
movw %di, %fs:(%bx)
movw %es, %fs:2(%bx) /* 把中断向量地址指向0x9000:0x4000-12 */
/* 4: 重新启动磁盘,使磁盘参数设置生效 */
load_setup:
xorb %ah, %ah /* ah = 0 -> reset disk */
xorb %dl, %dl /* dl = 0 -> driver = 0 */
int $0x13 /* reset disk */

/* 5: 把setup从第2扇区开始的4个扇区copy到0x90200 */
xorw %dx, %dx /* dl = 驱动器(drive) = 0, dh = 磁头(head) = 0 */
movb $0x02, %cl /* cl = 开始扇区(sector) = 2, 磁道(track) = 0 */
movw $0x0200, %bx /* address = 512, es:bx存放读取的数据; 把setup放在bootsect后 */
movb $0x02, %ah /* ah = 2, 在磁盘中断0x13中的作用是读扇区 */
movb setup_sects, %al /* al = 4, 所需读取的扇区数 */
int $0x13 /* read disk */
jnc ok_load_setup /* jnc根据标志(flag)寄存器中的cf来判断,cf=1不跳转,直接执行下面指令,cf=0则跳转;读磁盘出错cf=1,完成cf=0 */
/* 6: 读磁盘出错则执行以下代码 */
pushw %ax /* 把ax值压入堆栈; al = 4, ah = 2, 因为print_nl*/
call print_nl /* 打印回车换行 */
movw %sp, %bp /* 把sp->bp, ss:bp=ax ?????*/
call print_hex /* 打印bp所指向的数据, 即为ax寄存器信息 ?????*/
popw %ax
jmp load_setup
/* 7:获得磁盘扇区数,由36,18,15,9一个个试 */
ok_load_setup:
movw $disksizes, %si /* 注意disksizes前有"$",表示把扇区表首地址放入si中,用来测试四个扇区 */
probe_loop:
lodsb /* 指令功能:AL(AX) cbtw /* 操作数扩展指令,符号扩展:al->ax */
movw %ax, sectors /* 把ax中的值赋值给16位的sectors地址单元 */
cmpw $disksizes+4, %si /* 对于36,18,15来说, si - $disksizes jae got_sectors /* jae:(si - $disksizes)大于或等于时跳转, 当扇区值为9时就si = $disksizes+4, 发生跳转 */

/* 8: 读磁盘操作,如果扇区不对,则会产生错误,重新跳到probe_loop执行 */
xchgw %cx, %ax /* 交换寄存器值,把ax中的扇区数给cx;cx:扇区(sector),磁道(track);即读最大的扇区,如果可以读,扇区数就正确,否则就不正确 */
xorw %dx, %dx /* dl = 驱动器(drive) = 0, dh = 磁头(head) = 0 */
xorb %bl, %bl /* bl = 0x00 */
movb setup_sects, %bh /* bh = 0x04 */
incb %bh /* bh = 0x05 */
shlb %bh /* shl:位运算指令,bh左移一位: (0x05 0xa0; bx = 0x0a00, es:bx即为存放数据缓冲区,数据没什么用,只用来测试*/
movw $0x0201, %ax /* ah = 2,为读磁盘; al = 1,读一个扇区(sector) */
int $0x13 /* 磁盘(disk)中断 */
jc probe_loop /* jc:有进位时转移;对于磁盘读操作来讲,cf=1代表出错,cf=0读写正确;所以如果出错就继续probe,否则向下继续执行 */
/* 9: 已取得磁盘扇区数,接下来读取光标的位置,并打印"Loading"信息 */
got_sectors:
movw $INITSEG, %ax
movw %ax, %es
movb $0x03, %ah /* 对中断0x10来说,ah=0x03表示,读取光标的位置(cursor pos) */

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值