引导启动程序主要是指/boot目录中的三个汇编文件:
- bootsect.s
- head.s
- setup.s
- bootsect.s和setup.s采用近似于Intel的汇编语言语法,需要使用Intel 8086汇编编译器和连接器as86和ld86
- head.s使用GUN的汇编程序格式(AT&T语法),需要用GNU的as进行编译。
- 这里使用两种编译器的主要原因是由于对于Intel x86处理器系列来讲,GNU的编译器仅支持i386及以后出的CPU,不支持生成运行在实模式下的程序。
Linux启动流程:
- PC电源打开后,80x86结构的CPU将自动进入实模式,从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址;
- PC机的BIOS将执行某些系统的检测,并在物理地址0处开始初始化中断向量;
- 将可启动设备的第一个扇区(磁盘引导扇区,512字节,对应的是boot/bootsect.s)读入内存绝对地址0x7C00(31KB)处,并跳转到这个地方。
- boot/bootsect.s被执行时,会把自己移到绝对地址0x90000(576KB)处,并把启动设备中后2KB字节代码(boot/setup.s)读入到内存0x90200处,
- 内核的其他部分(system模块)则被读入到从地址0x10000开始处,当时system模块的长度不会超过0x80000字节大小(512KB),所以它不会覆盖在0x90000处开始的bootsect和setup模块
- setup程序将会把system模块移动到内存起始(0x00000)处,这杨system模块中代码的地址也即等于实际的物理地址,便于对内核代码和数据的操作。
- 进入保护模式并跳转至系统的余下部分(0x00000),此时所有32位运行方式的设置启动被完成:IDT、GDT以及LDT被加载,处理器和协处理器也已确认,分页工作也设置好了
- 调用init/main.c中的main()程序
bootsect.s
在as86汇编语言程序中,凡是以感叹号’!’或分号’;’开始的语句其后面均为注释文字。注释语句可以放在任何语句的后面,也可以从一个新行开始。
!
! 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
! SYS_SIZE 是要加载的节数(16字节为1节)。0x3000共为x30000字节=196kB
! 若以1024字节为1KB计,则应该是192KB
SYSSIZE = 0x3000 !指编译连接后system模块的大小,这里给出一个最大的默认值
!
! 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.
! bootsect.s被bios-启动子程序加载至0x7c00(31k)处,并将自己移到了地址
! 0x90000(576k)处,并跳转到那里
! It then loads 'setup' directly after itself (0x90200), and the system
! at 0x10000, using BIOS interrupts.
! 然后使用BIOS中断将‘setup’加载到0x90200,system 加载到地址0x10000处
! 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
! 注意!目前 system最大长度限制为512KB,即使实在将来这也应该没有问题。我想让他
! 简单明了。这样512KB的内核长度应该足够了,尤其是在这里没有像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.
! 加载程序已经做的够简单了,所以持续的读出错误将导致死循环,只能手工重启
!只要可能,通过一次读取所有的扇区,加载过程可以做的很快
!‘.global‘是汇编指示符(或称为汇编伪指令、伪操作符),用于定义随后的标号标识符
! 是外部的或全局的,并且即使不使用也强制引入。
!汇编指示符均以一个字符’.’开始,并且不会在编译时产生任何代码。
! 汇编指示符由1个伪操作码,后跟0个或多个操作数组成。
! ’global’是一个伪操作码,而其后面的标号’begtext, begdata, begbss’等就是它的操作数。
! 标号是后面带冒号的标识符,例如’begtext:’。但在引用一个标号时无须带冒号。
!’.text’、’.data’、’.bss’。它们分别对应汇编程序编译产生目标文件中的3个段,即正文段、数据段和未初始化数据段。
! ’.text’用于标识正文段的开始位置,并切换到text段;
! ’.data’用于标识数据段的开始位置,并把当前段切换到数据段;
! ’.bss’用于标识未初始化数据段的开始,并把当前段改变成bss段。
! 这里在每个段中定义一个标号,最后再切换到text段开始编写随后的代码。
! 把三个段都定义在同一重叠地址范围中,因此实际上不分段。
.globl begtext, begdata, begbss, endtext, enddata, endbss ! 定义了6个全局标识符
.text ! 文本段
begtext:
.data ! 数据段
begdata:
.bss ! 未初始化数据段(Block Started by Symbol)
begbss:
.text ! 文本段
! 等号’=’(或符号’EQU’)用于定义标识符BOOTSEG所代表的值,因此这个标识符可称为符号常量。这个值与C语言中的写法一样,可以使用十进制、八进制和十六进制。
SETUPLEN = 4 ! nr of setup-sectors setup程序的扇区数
BOOTSEG = 0x07c0 ! original address of boot-sector bootsect的原始地址(段地址)
INITSEG = 0x9000 ! we move boot here - out of the way 将bootsect移到这里
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 ! 指定根文件系统设备是第2个硬盘的第1个分区。这是Linux老式的硬盘命名方式,具体值的含义如下:
! 设备号 = 主设备号*256 + 次设备号 (也即 dev_no = (major<<8) + minor)
! (主设备号 : 1 -内存,2 - 磁盘,3 - 硬盘,4-ttyx,5-tty ,6-并行口,7-非命名管道)
! 0x300 - /dev/hd0 第1个硬盘
! 0x301 - /dev/hd1 第1个硬盘的第1个分区
! ...
! 0x304 - /dev/hd4 第1个盘的第4个分区
! 0x305 - /dev/hd5 第二个硬盘
! 0x306 - /dev/hd6 第二个硬盘的第1个分区
! ...
! 0x309 - /dev/hd9 第2个盘的第4个分区
! 标识符’entry’是保留关键字,用于迫使链接器ld86在生成的可执行文件中包括进其后指定的标号’start’。
entry start ! 告知连接程序,程序从start标号开始执行,指定程序的入口地址
start:
! 注意:程序运行在实模式下面,物理地址=代码段cs*16+偏移,要想在0x7c00处开始执行,要设BOOTSEG=0x07c0,而不是0x7c00。
mov ax,#BOOTSEG ! 将ds段寄存器置为 0x7c00
mov ds,ax
mov ax,#INITSEG ! 将es寄存器置为 0x9000
mov es,ax
mov cx,#256 ! 移动计数值 = 256 字
sub si,si ! 源地址 ds:si = 0x7c0:0x0000
sub di,di ! 目的地址 es:di = 0x9000:0x0000
rep ! 重复操作指令前缀,循环执行movw,且cx递减1,直到cx为0为止
movw ! 移动一个字,将DS:SI的16位bit数据复制送至ES:DI
jmpi go,INITSEG ! jmpi为段间跳转指令,执行这条指令之后,CS = INITSEG,IP = go
! 从下面开始,CPU执行已移动到0x9000段处的代码
go: mov ax,cs ! 将ds、es、ss都设置成移动到0x9000段处的代码
mov ds,ax ! 由于程序中有堆栈操作,因此必须设置堆栈
mov es,ax
! put stack at 0x9ff00. ! 将堆栈指针sp指向0x9ff00(即0x9000:0xff00处)
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
! 由于代码段移动过了,所以要重新设置堆栈段的位置。
! sp只要执行远大于512偏移(即地址0x90200)处都可以。因为
! 从0x90200地址开始处还要设置setup程序,而此时setup程序大约为4个扇区
! 因此sp要指向大于(0x200 + 0x200*4 + 堆栈大小)处
! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
! 在bootsect程序块后紧跟着加载setup模块的代码数据
! 注意es已经设置好了 0x9000、
load_setup:
! 利用BIOS中断INT 0x13将setup模块从磁盘第二个扇区开始读到0x90200处,共读4个扇区。如果读出错
! 则复位驱动器,并重试,没有退路。INT 0x13的使用方法如下:
! 读扇区:
! ah = 0x02 - 读磁盘扇区到内存;al = 需要读出的扇区数量
! ch = 磁道(柱面)号的低8位; cl = 开始扇区(0-5位),磁道号高2位(6-7)
! dh = 磁头号; dl = 驱动器号
! es:bx -> 指向数据缓冲区;如果出错则CF标志置位
! 汇编中的CF是什么意思?
! 进位标志CF(Carry Flag)。进位标志CF主要用来反映运算是否产生进位或借位。如果运算结果的最高位产生了一个进位或借位,那么,其值为1,否则其值为0。涉及到它的指令有两个:即JC在CF=1时跳转,JNC在CF=0时跳转。
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 进入读取磁盘的中断接口(0x13为指定读取磁盘中断号)
jnc ok_load_setup ! ok - continue 条件跳转指令,如果CF等于0(运算没有产生进位或借位),则跳转到ok_load_setup标签地址处执行代码.
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
! 取磁盘驱动器的参数,特别是每道的扇区数量
! 取磁盘驱动器参数INT 0x13调用格式和返回信息如下:
! ah = 0x80 dl = 驱动器号(如果是硬盘则要置位7为1)
! 返回信息:
! 如果出错则CF置位,并且ah=状态码
! ah = 0 , al = 0, bl = 驱动器类型(AT/PS2)
! ch = 最大磁道号的低8位, cl = 每磁道最大扇区数(位0-5),最大磁道号高位
! dh = 最大磁头数 , dl = 驱动器数量
! es:di -> 软驱磁盘参数表
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs ! 表示下一条语句的操作数在cs段寄存器所指的段中
mov sectors,cx ! 保存每磁道扇区数
mov ax,#INITSEG
mov es,ax # 因为上面取磁盘参数中断改掉了es的值,这里重新改回
! Print some inane message ! 显示一些信息
! INT 10H 中断读取光标,入参: AH =3 ,BH=页号
! 输出 : CH = 光标开始行 CL = 光标结束行 DH = 行 DL = 列
mov ah,#0x03 ! read cursor pos
xor bh,bh ! 读光标位置
int 0x10
mov cx,#24 ! 共24个字符
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1 ! 指向要显示的字符串
mov ax,#0x1301 ! write string, move cursor
int 0x10 ! 写字符串并移动光标
! ok, we've written the message, now
! we want to load the system (at 0x10000) ! 现在开始将system模块加载到0x10000(64k)处
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000 ! es = 存放system的段地址
call read_it ! 读磁盘上system模块,es为输入参数
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.
! 此后,我们检查要使用哪个根文件系统设备(简称根设备)。如果已经指定了设备(!=0)
! 就直接使用给定的设备。否则就需要根据BIOS报告的每磁道扇区来确定到底使用/dev/PS0(2,28)还是/dev/at0(2,8)
! 在Linux中软驱的主设备号是2,次设备号 = type*4 +nr,其中nr为0-3分别对应软驱A、B、C、D;type是软驱类型
! 2->1.2M 7->1.44M 因为7*4+0 =28,所以/dev/PS0(2,28)指的是1.44M A 驱动器,其设备号是0x021c,/dev/at0(2,8)
! 指的是1.2M A驱动器,其设备号是0x0208
seg cs
mov ax,root_dev
cmp ax,#0
jne root_defined
seg cs
mov bx,sectors ! 取保存的每磁道扇区数,如果sectors =15则说明是1.2Mb的驱动器;
! 如果sectors = 18 则说明是1.44Mb软驱,因为是可引导的驱动器,所以肯定是A驱
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15 ! 判断每磁道扇区是否=15
je root_defined ! 如果等于,则ax中就是引导驱动器的设备号
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:
! 到此,所有程序都加载完毕,我们就跳转到被加载在bootsect后面的setup程序去
jmpi 0,SETUPSEG ! 跳转到0x9020:0000(setup.s程序的开始处)
! !!!本程序到此就结束了!!!!!
! 下面是两个子程序
! This routine loads the system at address 0x10000, making sure
! no 64kB boundaries are crossed. We try to load it as fast as
! possible, loading whole tracks whenever we can.
! 该子系统将系统模块记载到内存地址0x10000处,并确定没有跨越64KB的
! 内存边界,我们试图尽快地进行加载,只要可能,就每次加载整条磁道的数据
! in: es - starting address segment (normally 0x1000)
! 输入:es - 开始内存地址段值(通常是0x1000)
sread: .word 1+SETUPLEN ! sectors read of current track
! 当前磁道中已读的扇区数。开始时已经读进1扇区的引导扇区
head: .word 0 ! current head 当前磁头号
track: .word 0 ! current track 当前磁道号
read_it:
! 测试输入的段值。从盘上读入的数据必须存放在位于内存地址64KB的边界开始处,否则进入死循环。
! 清bx寄存器,用于表示当前内存放数据的开始位置。
mov ax,es
test ax,#0x0fff ! 将两个操作数进行逻辑与运算,,运算结果为0时置1,否则置0.
die: jne die ! es must be at 64kB boundary 当零标志 Z=0 则跳转; 否则 零标志 Z=1 则顺序执行下一条指令。
xor bx,bx ! bx is starting address within segment 异或
rp_read:
! 判断是否已经读入全部数据。比较当前所读段是否就是系统数据末端所处的段(#ENDSEG),
! 如果不是就跳转值下面ok1_read标号处继续读数据。否则退出子程序返回
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet? 是否已经加载了全部数据 实际上求(ax-ENDSEG)
jb ok1_read ! 当CF=1(ax<ENDSEG, 有借位)时跳转到ok1_read
ret ! 当ax>=ENDSEG时返回(我认为不会出现大于的情况)
ok1_read:
! 计算和验证当前磁道需要读取的扇区数,放在ax寄存器中。
! 根据当前磁道还未读取的扇区数以及段内数据字节开始偏移位置,计算如果全部
! 读取这些未读扇区,所读总字节数是否超过64KB段长度的限制,若会超过,则根据此次
! 最多能读入的字节数(64KB-段偏移位置),反算出此次需要读取的扇区数。
seg cs
mov ax,sectors ! 这两句相当于 mov ax, cs:[sectors]; 取每磁道扇区数
sub ax,sread ! ax = ax - sread,减去当前磁道已读扇区数
mov cx,ax ! 当前磁道未读扇区数
shl cx,#9 ! cx=cx*512 字节
add cx,bx ! 以上3行相当于 cx = ax * 512 + bx
! 假设再读ax个扇区,cx就是段内共读入的字节数
jnc ok2_read ! 若cx < 0x10000(CF=0,没有进位)则跳转到ok2_read,若没有超过64KB字节,则跳转至ok2_read处执行
je ok2_read ! 若cx = 0(ZF=1),说明刚好读入64KB,则跳转到ok2_read
xor ax,ax ! ax = 0x0000
sub ax,bx ! 求bx对0x10000的补数,结果在ax中
shr ax,#9 ! 除以512,得到扇区数,AL作为参数,传给read_track
ok2_read:
call read_track
mov cx,ax ! cx = 该次操作已读取的扇区数
add ax,sread ! 当前磁道上已读取的扇区数
seg cs
cmp ax,sectors ! 如果当前磁道上的还有扇区未读,则跳转到ok3_read
jne ok3_read
! 读该磁道的下一磁头面(1号磁头)上的数据。如果已经完成,则去读下一磁道
mov ax,#1
sub ax,head ! 判断当前磁头号
jne ok4_read ! 如果是0磁头,则再去读1磁头面上的扇区数据
inc track ! 否则去读下一磁道
ok4_read:
mov head,ax ! 保存当前磁头号
xor ax,ax ! 清当前磁道已读扇区数
ok3_read:
mov sread,ax ! 保存当前磁道已读扇区数
shl cx,#9 ! 上次已读扇区数*512字节
add bx,cx ! 调整当前段内数据开始位置
jnc rp_read ! 若小于64KB边界值,则跳转到rp_read处,继续读数据
! 否则调整当前段,为读下一段数据作准备
mov ax,es
add ax,#0x1000 !将段基址调整为指向下一个64KB内存开始处
mov es,ax
xor bx,bx ! 清段内数据开始偏移值
jmp rp_read ! 跳转值rp_read,继续读取数据
! 读当前磁道上指定开始扇区和需读扇区数的数据到es:bx开始处
! al - 需读扇区数;ex:bx 缓冲区开始位置
!
read_track:
push ax
push bx
push cx
push dx
mov dx,track ! 取当前磁道号
mov cx,sread ! 取当前磁道上已读扇区数
inc cx ! cl = 开始读扇区
mov ch,dl ! ch = 当前磁道号
mov dx,head ! 取当前磁头号
mov dh,dl ! dh = 磁头号
mov dl,#0 ! dl = 驱动器号(0表示当前A驱动器)
and dx,#0x0100 ! 磁头号不大于1
mov ah,#2 ! ah = 2,读磁盘扇区功能号
int 0x13
jc bad_rt ! 若出错,则跳转至bad_rt
pop dx
pop cx
pop bx
pop ax
ret
! 执行驱动器复位操作(磁盘中断功能号0),再跳转到read_track处重试
bad_rt: mov ax,#0
mov dx,#0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
/*
* This procedure turns off the floppy drive motor, so
* that we enter the kernel in a known state, and
* don't have to worry about it later.
*/
! 这个子程序用于关闭软驱的马达,这样我们进入内核后它处于已知状态,以后也就无须担心它了
kill_motor:
push dx
mov dx,#0x3f2 ! 软驱动控制卡的驱动端口,只写
mov al,#0 ! A驱动器,关闭FDC,禁止DMA和中断请求,关闭马达
outb ! 将al中的内容输出到dx指定的端口去
pop dx
ret
sectors:
.word 0 ! 存放当前软盘每磁道的扇区数
msg1:
.byte 13,10 ! 回车、换行的ASII码
.ascii "Loading system ..."
.byte 13,10,13,10 ! 共24ge ASCII码字符
.org 508 ! 表示下面语句从地址508(0x1FC)开始,所以root_dev
! 在启动扇区的第508开始的2个字节
root_dev:
.word ROOT_DEV ! 这里存放根文件系统所在的设备号(init/main.c会使用)
boot_flag:
.word 0xAA55 ! 硬盘有效标识
.text
endtext:
.data
enddata:
.bss
endbss: