Linux引导启动程序(boot)
本章主要描述boot/目录中的三个汇编代码文件,见列表6-1所示。正如在前一章中提到的,这三个文件虽然都是汇编程序,但却使用了两种语法格式。bootsect.s和setup.s是实模式下运行的16位代码程序,采用近似于Intel的汇编语言并且需要使用Intel8086汇编编译器和连接器as86和ld86,而head.s则使用GNU的汇编程序格式,并且运行在保护模式下,需要用GNU的as(gas)进行编译。这是一种AT&T语法的汇编语言程序。
Linus当时使用两种汇编编译器的主要原因在于对于Intel x86处理器系列来讲,linus那时的GNU汇编编译器仅能支持i386以及后出的CPU代码指令,若不采用特殊方法就不能支持生成运行在实模式下的16位代码程序。直到1994年以后发布的GNU as汇编器才开始支持编译16位代码的.code16伪指令。参见GNU汇编器(Using as -The GNU Assembler)中"80386相关特性"一节中"编写16位代码"小节。但直到内核版本2.4.X起,bootsect.s和恶setup.s程序才完全使用统一的as来编写。
6.1总体功能
这里先总体说明一下Linux操作系统启动部分的主要执行流程。当PC的电源打开后,80x86结构的CPU将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。PC机的BIOS将执行某些系统的检测,并在物理地址0处开始初始化中断向量。此后,它将可启动设备的第一个扇区(磁盘引导扇区,512字节)读入内存绝对地址0x7C00处,并跳转到这个地方。启动设备通常是软驱或是硬盘。这里的叙述是非常简单的,但这已经足够理解内核初始化的工作过程了。
Linux的最最前面部分是用8086汇编语言编写的(boot/bootsect.s),它将由BIOS读入到内存绝对地址0x7C00(31KB)处,当它被执行时就会把自己移动到内存绝对地址0x90000(576KB)处,并把启动设备中后2KB字节代码(boot/setup.s)读入到内存0x90200处,而内核的其他部分(system模块)则被读入到从内存地址0x10000(64KB)开始处,因此从机器加电开始顺序执行的程序见图6-1所示。
因为当时system模块的长度不会超过0x80000字节大小(即512KB),所以bootsect程序把system模块读入物理地址0x10000开始位置处时并不会覆盖在0x90000(576KB)处开始的bootsect和setup模块。后面setup程序将会把system模块移动到物理内存起始位置处,这样system模块中代码的地址也即等于实际的物理地址,便于对内核代码和数据进行操作。图6-2清晰地显示出Linux系统启动时这几个程序或模块在内存中的动态位置。其中,每一竖条框代表某一时刻内存中各程序的映像位置图。在系统加载期间将显示信息"Loading...."。然后控制权将传递给boot/setup.s中的代码,这是另一个实模式汇编语言程序。
启动部分识别主机的某些特性以及VGA卡的类型。如果需要,它会要求用户为控制台选择显示模式。然后将整个系统从地址0x10000移至0x0000处,进入保护模式并跳转至系统的余下部分(在0x0000处)。此时所有32位运行方式的设置启动被完成:IDT、GDT以及LDT被加载,处理器和协处理器也已确认,分页工作也设置好了;最终调用init/main.c中的main()程序。上述操作的源代码是在boot/head.s中的,这可能是整个内核中最有诀窍的代码了。注意如果在前述任何一步中出了错,计算机就会死锁。在操作系统还没有完全运转之前是处理不了出错的。
bootsect的代码为什么不把系统模块直接加载到物理地址0x0000开始处而要在setup程序中再进行移动呢?这是因为在随后执行的setup代码开始部分还需要利用ROM BIOS中的中断调用来获取机器参数(例如显示卡模式、硬盘参数表等)。当BIOS初始化时会在物理内存开始处放置一个大小为0x400字节(1KB)的中断向量表,因此需要在使用完BIOS的中断调用后才能将这个区域覆盖掉。
另外,仅在内存中加载了上述内核代码模块并不能让Linux系统运行起来。作为完整可运行的Linux系统还需要有一个基本的文件系统支持,即根文件系统。Linux0.11内核仅支持MINIX的1.0文件系统。根文件系统通常是在另一软盘上或者在一个硬盘分区中。为了通知内核所需要的根文件系统在什么地方,bootsect.s程序的第43行上给出了根文件系统所在的默认块设备号。块设备号的含义请参见程序中的注释。在内核初始化时会使用编译内核时放在引导扇区第509、510(0x1fc--0x1fd)字节中的指定设备号。
6.2bootsect.s程序
6.2.1功能描述
bootsect.s代码是磁盘引导块程序,驻留在磁盘的第一个扇区中(引导扇区,0磁道(柱面),0磁头,第1个扇区)。在PC机加电ROM BIOS自检后,ROM BIOS会把引导扇区代码bootsect加载到内存地址0x7C00开始并执行。在bootsect代码执行期间,它会将自己移动到内存绝对地址0x90000开始处并继续执行。该程序的主要作用是首先把从磁盘第2个扇区开始的4个扇区的setup模块(由setup.s编译而成)加载到内存紧接着bootsect后面位置处(0x90200),然后利用BIOS中断0x13取磁盘参数表中当前启动引导盘的参数,接着载屏幕显示"Loading system..."字符串。再者把磁盘setup模块后面的system模块加载到内存0x10000开始的地方。随后确定根文件系统的设备号,若没有指定,则根据所保存的引导盘的每磁道扇区数判别出盘的类型和种类(是1.44M A盘吗?)并保存其设备号root_dev(引导块的5008地址处),最后长跳转到setup程序的开始处(0x90200)执行setup程序。在磁盘上,引导块、setup和system模块的扇区位置和大小示意图6-3
图中示出了Linux0.11内核在1.44 磁盘上所占扇区的分布情况。1.44MB磁盘共有2880个扇区,其中引导程序代码占用第1个扇区,setup模块用随后的4个扇区,而0.11内核system模块大约占随后的240个扇区。还剩下2630多个扇区未被使用。这些剩余的未用空间可被用来用来存放一个基本的根文件系统,从而可以创建出单张磁盘就能让系统转起来的集成盘来。这将在块设备驱动程序一章中再作详细介绍。
6.2.2代码注释
程序6-1 linux/boot/bootsect.s
!
! 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是2要加载的系统模块长度,单位是节,16字节为1节。0x3000共为
!0x30000字节=196kB(若以1024字节为1KB计,则应该是192KB),对应当前的版本空间已足够了。
! 下面等号'='或符号'EQU'用于定义标识符或标号所代表的值,可称为符号常量。这个常量指明编译连接后system模块的大小。这个等式"SYSSIZE = 0x3000"原来是由linux/Makefile中第92行上的语句动态自动产生。但从Linux0.11版本开始就在这里给出了一个最大默认值。原来参见的自动产生语句还没有被删除,参见程序5-1中第92行的说明。当该值为0x8000时,表示内核最大为512KB。
! 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.
!以下是前面这些文字的翻译:
bootsect.s C 1991 Linux Torvalds版权所有
bootsect.s被bios一启动子程序加载至0x7c00(31KB)处,并将自己移到了地址0x90000(576KB)处,并跳转到那里。
它然后使用BIOS中断将'setup'直接加载到自己的后面(0x902000)(576.5KB),并将system加载到地址0x10000处。
注意!目前的内核系统最大长度限制为(8*65536)(512KB)字节,即使是在将来这也应该没有问题的。我想让它保持简单明了。这样512KB的最大内核长度应该足够了,尤其是这里没有像minix中一样包含缓冲区高速缓冲。
加载程序已经做得够简单了,所以持续的读出错将导致死循坏。只能手工重启。
只要可能,通过一次读取所有的扇区,加载过程可以做得很快。
伪指令(伪操作符).global或.global用于定义随后得标识符是外部的或全局的,并且即使不使用也强制引入。.text、.data和.bss用于分别定义当前代码段、数据段和未初始化数据段。在链接多个目标模块时,链接程序(ld86)会根据它们的类别把各个目标模块中的相应段分别组合(合并)在一起。这里把三个段都定义在同一重叠地址范围中,因此本程序实际上不分段。另外,后面带冒号的字符串是标号,例如下面的'begtext:'。一条汇编语句通常由标号(可选)、指令助记符(指令名)和操作数三个字段组成。标号位于一条指令的第一个字段。它代表其所在位置的地址,通常指明一个跳转指令的目标位置。
.globl begtext, begdata, begbss, endtext, enddata, endbss!定义了6个全局标识符;
.text !文本段;
begtext:
.data !数据段;
begdata:
.bss !未初始化数据段(Block Started by Symbol);
begbss:
.text !文本段;
SETUPLEN = 4 ! nr of setup-sectors
//setup程序的扇区数(setup-sectors)值;
BOOTSEG = 0x07c0 ! original address of boot-sector
//bootsect的原始地址(是段地址,以下同):
INITSEG = 0x9000 ! we move boot here - out of the way
// 将bootsect移动这里 -- 避开;
SETUPSEG = 0x9020 ! setup starts here
//setup程序从这里开始;
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
//system 模块加载到0x10000(64KB)处;
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
//设备号0x306指定根文件系统设备是第2个硬盘的第一个分区。当年Linus是在第2个硬盘上安装了Linux0.11系统,所以这里ROOT_DEV被设置为0x306。在编译这个内核时可以根据自己根文件系统所在设备位置修改这个设备号。这个设备号是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 -代表整个第2个硬盘;
0x306 -/dev/hd6 -第2个盘的第1个分区;
...
0x309 /dev/hd9 -第2个盘的第4个分区;
!从Linux内存0.95版后就已经使用与现在内核相同的命名方法了。
伪指令entry迫使链接程序在生成的执行程序(a.out)中包含指定的标识符或标号
47--56行作用是将自身(bootsect)从目前段位置0x07c0(31KB)移动到0x9000(576KB)处,共256字(512字节),然后跳转到移动后代码的go标号处,也即本程序的下一语句处。
entry start !告知链接程序,程序从start标号开始执行。
start:
mov ax,#BOOTSEG !将ds段寄存器置为0x7C0;
mov ds,ax
mov ax,#INITSEG !将es段寄存器设置为0x9000;
mov es,ax
mov cx,#256 !设置移动计数值=256字
sub si,si !源地址 ds:si = 0x07C0:0x0000
sub di,di !目的地址 es:di =0x9000:.0x0000
rep !重复执行并递减cx的值,直到cx=0为止。
movw !即movs指令。这里从内存[si]处移动cx个字到[di]处。
jmpi go,INITSEG !段间跳转(Jump Intersegment)。这里INITSEG指出跳转到的段地址,标号go是段内偏移地址。
//从下面开始,CPU在已移动到0x90000位置处的代码中执行。这段代码设置几个段寄存器,包括栈寄存器ss和sp。栈指针sp只要指向远大于512字节偏移(即地址0x90200)处都可以。因为从0x90200地址开始处还要放置setup程序,而此时setup程序大约为4个扇区,因此sp要指向大于(0x200+0x200*4+堆栈大小)处。
go: mov ax,cs !将ds、es和ss都置成移动后代码所在的段处(0x9000)。
mov ds,ax !由于程序中有栈操作(push,pop,call),因此必须设置堆栈
mov es,ax
! put stack at 0x9ff00. !将堆栈指针sp指向0x9ff0(即0x9000:0xff00)处。
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
!在bootsect程序块后紧跟着加载setup模块的代码数据。
!注意es已经设置好了。(在移动代码时,es已经 指向目的段地址处0x9000)。
! 68-77行的用途是利用BIOS中断INT 0x13将setup模块从磁盘第2个扇区开始读到0x90200开始处,共读4个扇区。如果读出错,则复位驱动器,并重试,没有退路。INT 0x13的使用方法如下:
ah = 0x02 - 读磁盘扇区到内存;al = 需要读取的扇区数量:
ch = 磁道(柱面)号的低8位;cl = 开始扇区(位0-5),磁道号高2位(位6-7);
dh = 磁头号; dl = 驱动器号(如果是硬盘则位7要置位);
es:bx->指向数据缓冲区; 如果出错则CF标志位,ah中是出错吗。
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
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
取磁盘驱动器的参数,特别是每道的扇区数量。
取磁盘驱动器参数INT 0x13调用格式和返回信息如下:
ah = 0x08 dl=驱动器号(如果是硬盘则要置位7为1)。
返回信息:
如果出错则CF置位,并且ah =状态码。
ah=0,al=0, bl=驱动器类型(AT/PS2)
ch = 最大磁道号的低8位,cl=每磁道最大扇区数(位0-5),最大磁道号高2位(位6-7)
dh = 最大磁头数, dl =驱动器数量,
es:di->软驱磁盘参数表。
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
下面指令表示下一条语句的操作数在cs段寄存器所指的段中。它只影响其下一条语句。实际上,由于本程序代码和数据都被设置处于同一个段中,即段寄存器cs和ds、es的值相同,因此本程序中此处可以不使用该指令。
seg cs
下句保存的每磁道扇区数。对于软盘来说(d1=0),其最大磁道号不会超过256,ch已经足够表示它,因此cl的位6-7肯定为0。又86行已置ch=0,因此此时cx中是每磁道扇区数。
mov sectors,cx
mov ax,#INITSEG
mov es,ax !因为上面取磁盘参数中断改掉es的值,这里重新改回。
! Print some inane message
//显示信息:"Loading system...."回车换行,共显示包含回车和换行控制字符在内的24个字符。BIOS中断0x10功能号ah=0x03,读光标位置。
输入:bh=页号
返回:ch=扫描开始线,cl=扫描结束线:dh = 行号(0x00顶端);dl=列号(0x00最左边)。
BIOS中断0x10功能号ah = 0x13,显示字符串。
输入:al = 放置光标的方式及规定属性。0x01-表示使用bl中的属性值,光标停在字符串结尾处。es:bp此寄存器对指向要显示的字符串起始位置处。cx=显示的字符串字符数。bh=显示页面号;bl=字符属性。dh=行号;dl=列号。
mov ah,#0x03 ! read cursor pos
xor bh,bh !首先读光标位置。返回光标位置值在dx中。
int 0x10 ! dh = 行(0--24);dl=列(0--79)。供显示串用。
mov cx,#24 !共显示24个字符
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1 !es:bp寄存器对指向要显示的字符串。
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(64KB)开始处。
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.
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
! 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.
!
! in: es - starting address segment (normally 0x1000)
!
sread: .word 1+SETUPLEN ! sectors read of current track
head: .word 0 ! current head
track: .word 0 ! current track
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
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
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
ok2_read:
call read_track
mov cx,ax
add ax,sread
seg cs
cmp ax,sectors
jne ok3_read
mov ax,#1
sub ax,head
jne ok4_read
inc track
ok4_read:
mov head,ax
xor ax,ax
ok3_read:
mov sread,ax
shl cx,#9
add bx,cx
jnc rp_read
mov ax,es
add ax,#0x1000
mov es,ax
xor bx,bx
jmp rp_read
read_track:
push ax
push bx
push cx
push dx
mov dx,track
mov cx,sread
inc cx
mov ch,dl
mov dx,head
mov dh,dl
mov dl,#0
and dx,#0x0100
mov ah,#2
int 0x13
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
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.
*/
/*
*这个子程序用于关闭软驱的马达,这样我们进入内核后就能知道它所处的状态,以后也就无须担心它了
*/
/* 下面第235行上的值0x3f2是软盘控制器的一个端口,被称为数字输出寄存器(D0R)端口。它是一个8位的寄存器,其中7--位4分别用于控制4个软驱(D--A)的启动和关闭。位3--位2用于允许/禁止DMA和中断请求以及启功/复位软盘控制器FDC。位1--位0用于选择操作的软驱。第236行上在al中设置并输出的0值,就是用于选择A驱动器,关闭FDC,禁止DMA和中断请求,关闭马达。有关软驱控制卡编程的详细信息请参见kernel/blk_drv/floppy.程序后面的说明
kill_motor:
push dx
mov dx,#0x3f2 ! 软驱控制卡的数字输出寄存器(DOR)端口,只写。
mov al,#0 ! A驱动器,关闭FDC,禁止DMA和中断请求,关闭马达。
outb ! 将al中的内容输出到dx指定的端口去。
pop dx
ret
sectors:
.word 0 !存放当前启动软盘每磁道的扇区数。
msg1: !调用BIOS中断显示的信息。
.byte 13,10 !回车、换行的ASCII码。
.ascii "Loading system ..."
.byte 13,10,13,10 ! 共24个ASCII码字符。
!表示下面语句从地址508(0x1FC)开始,所以root_dev在启动扇区的第508开始的2个字节中
.org 508
root_dev:
.word ROOT_DEV !这里存放根文件系统所在设备号(init/main.c中会用)
!下面是启动盘具有有效引导扇区的标志。仅供BIOS中的程序加载引导扇区时识别使用。它必须位于引导扇区的最后两个字节中
boot_flag:
.word 0xAA55
.text
endtext:
.data
enddata:
.bss
endbss:
6.2.3其他信息
对bootsect.s这段程序的说明和描述,由于这段程序是在386实模式下运行的,因此相对来将比较容易理解。若此时阅读仍然有困难,那么建议你首先再复习一下80x86汇编及其硬件的相关知识,然后再继续阅读本书。对于最新开发的Linux内核,这段程序的改动也很小,基本保持0.11版bootsect程序的模样。
6.2.3.1Linux0.11硬盘设备号
程序中涉及的硬盘设备命名方式如下:硬盘的主设备号是3。其他设备的主设备号分别为:1-内存、2-磁盘、3-硬盘、4-ttyx、5-tty、6-并行口、7-非命名管道。由于1个硬盘中可以有1--4分区,因此硬盘还依据分区的不同用次设备号进行指定分区。因此硬盘的逻辑设备号由以下方式构成:设备号=主设备号*256+次设备号。两个硬盘的所有逻辑设备号见表6-1所示。
其中0x300和0x305并不与哪个分区对应,而是代表整个硬盘。从Linux内核0.95版本已经不使用这种烦琐的命名方式,而是使用于与现在相同的命名方法了。
6.2.3.2从硬盘启动系统
若需要从硬盘设备启动系统,那么通常需要使用其他多操作系统引导程序来引导系统加载。例如Shoelace、LILO或GrubD等多操作系统引导程序。此时bootsect.s所完成的任务会由这些程序来完成。bootsect程序就不会被执行了。因为如果从硬盘启动系统,那么通常内核映像文件Image会存放再活动分区的根文件系统中。因此你就需要知道内核映像文件Image处于文件系统中的位置以及是什么文件系统。即你的引导扇区程序需要能够识别并访问文件系统,并从中读取内核映像文件。
从硬盘启动的基本流程是:系统上电后,可启动硬盘的第1个扇区(主引导记录MBR-MasterBootRecord)会被BIOS加载到内存0x7c00处并开始执行。该程序会首先把自己向下移动到内存0x600处,然后根据MBR中分区表信息所指明活动分区中的第1个扇区(引导扇区)加载到内存0x7c00处,然后开始执行之。如果直接使用这种方式来引导系统会碰到这样一个问题,即根文件系统不能与内核映像文件Image共存。
我所想到的解决办法有两个。一种办法是专门设置一个小容量的活动分区来存放内核映像文件Image。而相应的根文件系统则放在另外一个分区中。这样虽然浪费了硬盘的4个主分区之一,但应该能在对bootsect.s程序作最少修改的前提下做到从硬盘启动系统。另一个办法是把内核映像文件Image与根文件系统组合存放在一个分区中,即内核映像文件Image放在分区开始的一些扇区中,而根文件系统则从随后某一指定扇区开始存放。这两种方法需要对代码进行一些修改。读者可以参考最后一章内容使用bochs模拟系统亲手做一些实验。
6.3setup.s程序
6.3.1功能描述
setup.s是一个操作系统加载程序,它的主要作用是利用ROM BIOS中断读取及其系统数据,并将这些数据保存到0x9000开始的位置(覆盖掉了bootsect程序所在的地方),所取得的参数和保留的内存位置见表6-2所示。这些参数将被内核中相关程序使用,例如字符设备驱动程序集中的console.c和tty_io.c程序等。
然后setup程序将system模块从0x10000-0x8fff(当时认为内核系统模块system的长度不会超过此值:512KB)整块向下移动到内存绝对地址0x0000处。接下来加载中断描述符表寄存器(idtr)和全局描述符表寄存器(gdtr),开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20-0x2f。最后设置CPU的控制寄存器CR0(也称机器状态字),从而进入32位保护模式进行,并跳转到位于system模块最前面部分的head.s程序继续运行。
为了能让head.s在32保护模式下运行,在本程序中临时设置了中断描述符表(IDT)和全局描述符表(GDT),并在GDT中设置了当前内核代码段的描述符和数据段的描述符。下面在head.s程序中会根据内核的需要重新设置这些描述符表。
下面首先简单介绍一下段描述符的格式、描述符表的结构和段选择符(有些书中称之位选择之)的格式。Linux内核代码中用到的代码段、数据段描述符的格式见图6-4所示。其中各字段的含义请参见第4章中的说明。
段描述符存放在描述附表中。描述符表其实就是内存中描述符项的一个阵列。描述符表有两类:全局描述表(Global descriptor table -GDT)和局部描述符表(Local descriptor table -LDT)。处理器是通过使用GDTR和LDTR寄存器来定位GDT表和当前的LDT表。这两个寄存器以线性地址的方式保存了描述符表的基地址和表的长度。指令lgdt和sgdt用于访问GDTR寄存器。头两个字节代表描述符表的长度,后4个字节是描述符表的基地址。然而请注意,访问LDTR寄存器的指令lldt所使用的操作数却一个2字节的操作数,表示全局描述符表GDT中的一个描述符项的选择符。该选择符所对应的GDT表中的描述符项应该对应一个局部描述符表。
例如,setup.s程序设置的GDT描述符项(见程序第 207--216行),代码段描述符的值是0x00C09A00000007FF,表示代码段的限长是8MB (=(0x7FF+1)*4KB,这里加1是因为限长值是从0开始算起的),段在线性地址空间中的基地是0,段类型值0x9A表示该段在于内存中、段的特权级别为0,段类型是可读可执行的代码段,段代码是32位的并且段的颗粒度是4KB。数据段描述符的值是0x00C09200000007FF,表示数据段的限长是8MB,段在线性地址空间中的基址是0,段类型值0x92表示该段存在于内存中,段的特权级别为0,段类型是可读可写的数据段,段代码是32位并且段的颗粒度是4KB。
这里再对选择符进行一些说明。逻辑地址的选择符部分用于指定一描述符,它是通过指定一描述符表并且索引其中的一个描述符项完成的。图6-5示出了选择符的格式。
其中索引值(Index)用于选择指定描述符表中8192(2的13次方)个描述符中的一个。处理器将该索引值乘8,并加上描述表的基地址即可访问表中指定的段描述符。表指示器(Table Indicator -TI)用于指定选择符所引用的描述符表。值为0表示指定GDT表,值为1表示指定当前的LDT表。请求者特权级(Requestor'sPrivalegeLevel-RPL)用于保护基址。
由于GDT表的第一项(索引值为0)没有被使用,因此一个具有索引值0和表指示器值也为0的选择符(也即指向GDT的第一项的选择符)可以用作一个空(null)选择符。当一个段寄存器(不能是CS或SS)加载了一个空选择符时,处理器并不会产生一个异常。但是若使用这个段寄存器访问内存时就会产生一个异常。对于初始化还未使用的段寄存器以陷入意外的引用来说,这个特性是很有用到。
在进入保护模式之前,我们必须首先设置好将要用到的段描述符表,例如全局描述符表GDT。然后使用指令lgdt把描述符表的基地址告知CPU(GDT表的基地址存放gdtr寄存器)。在将机器状态字的保护模式标志置位即可进入32位保护运行模式。
6.3.2代码注释
!
! setup.s (C) 1991 Linus Torvalds
!
! setup.s is responsible for getting the system data from the BIOS,
! and putting them into the appropriate places in system memory.
! both setup.s and system has been loaded by the bootblock.
!
! This code asks the bios for memory/disk/other parameters, and
! puts them in a "safe" place: 0x90000-0x901FF, ie where the
! boot-block used to be. It is then up to the protected mode
! system to read them from there before the area is overwritten
! for buffer-blocks.
!
setup.s负责从BIOS中获取系统数据,并将这些数据放到系统内存的适当地方。此时setup.s和system已经由bootsect引导块加载到内存中。
这段代码询问bios有关内存/磁盘/其他参数,并将这些参数放到一个"安全的"地方:0x90000-0x901FF,也即原来bootsect代码块曾经在的地方,然后在被缓冲块覆盖掉之前由保护模式的system读取。
! NOTE! These had better be the same as in bootsect.s!
以下这些参数最好和bootsect.s中的相同!
INITSEG = 0x9000 ! we move boot here - out of the way!原来bootsect所处的段。
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).!system在0x10000(64KB)处。
SETUPSEG = 0x9020 ! this is the current segment
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
entry start
start:
! ok, the read went well so we get current cursor position and save it for
! posterity.
!ok,整个读磁盘过程都很正确,现在保存光标位置以备今后使用。
mov ax,#INITSEG ! this is done in bootsect already, but...
mov ds,ax
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.
! Get memory size (extended mem, kB)
! 取扩展内存的大小值(KB)
! 利用BIOS中断0x15功能号ah=0x88取系统所含扩展内存大小并保存在内存0x90000处。
!返回:ax =从0x100000(1M)处开始的扩展内存大小(KB)。若出错则CF置位,ax =出错码。
mov ah,#0x88
int 0x15
mov [2],ax !将扩展内存数值存在0x90002处(1个字)
! Get video-card data:
! 下面这段用于取显示卡当前显示模式。
! 调用BIOS中断0x10,功能号ah=0x0f
! 返回:ah=字符列数;al = 显示模式;bh=当前显示页。
! 0x90004(1字)存放当前页;0x90006存放显示模式;0x90007存放字符列数。
mov ah,#0x0f
int 0x10
mov [4],bx ! bh = display page
mov [6],ax ! al = video mode, ah = window width
! check for EGA/VGA and some config parameters
! 检查显示方式(EGA/VGA)并取参数。
! 调用BIOS中断0x10,附加功能选择方式信息。功能号:ah = 0x12,b1 =0x10
! 返回:bh=显示状态。0x00-彩色模式,I/O端口的0x3dX;0x01-单色模式,I/O端口=0x3bX。
! bl =安装显示内存。0x00-64k;0x01--128k;0x02-192k;0x03 =256。
! cx =显示卡特性参数(参见程序后对BIOS视频中断0x10的说明)。
mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax ! 0x90008=??
mov [10],bx ! 0x9000A =安装的显示内存,0x9000B=显示状态(彩色/单色)
mov [12],cx ! 0x9000C =显示卡特性参数。
! Get hd0 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb
! Get hd1 data
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46]
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb
! Check that there IS a hd1 :-)
mov ax,#0x01500
mov dl,#0x81
int 0x13
jc no_disk1
cmp ah,#3
je is_disk1
no_disk1:
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
is_disk1:
! now we want to move to protected mode ...
cli ! no interrupts allowed !
! first we move the system to it's rightful place
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ! source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw
jmp do_move
! then we load the segment descriptors
end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
! that was painless, now we enable A20
call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.
mov al,#0x11 ! initialization sequence
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2
out #0xA0,al ! and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 ! start of hardware int's (0x20)
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 ! start of hardware int's 2 (0x28)
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 ! 8259-1 is master
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 ! 8259-2 is slave
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 ! 8086 mode for both
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0xFF ! mask off all interrupts for now
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
! This routine checks that the keyboard command queue is empty
! No timeout is used - if this hangs there is something wrong with
! the machine, and we probably couldn't proceed anyway.
empty_8042:
.word 0x00eb,0x00eb
in al,#0x64 ! 8042 status port
test al,#2 ! is input buffer full?
jnz empty_8042 ! yes - loop
ret
gdt:
.word 0,0,0,0 ! dummy
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L
gdt_48:
.word 0x800 ! gdt limit=2048, 256 GDT entries
.word 512+gdt,0x9 ! gdt base = 0X9xxxx
.text
endtext:
.data
enddata:
.bss
endbss:
6.3.3其他信息
为了获取机器的基本参数,这段程序多次调用了BIOS中的中断,并开始涉及一些对硬件端口的操作。下面简要地描述程序中使用到的BIOS中断调用,并A20地址线问题的缘由进行解释,最后提及关于Intel32位保护模式运行的问题。
6.3.3.1当前内存映像
在setup.s程序执行结束后,系统模块system被移动到物理地址0x0000开始处,而从位置0x90000开始处则存放了内核将会使用的一些基本参数,示意图如图6-6所示。
此时临时全局表中有三个描述符,第一个是NULL不使用,另外两个分别是代码段段描述符和数据段符。它们都指向系统模块的起始处,也即物理地址0x0000处。这样当setup.s中执行最后一条指令'jmp 0,8'(第193行)时,就会跳到head.s程序开始处继续执行下去。这条指令中的'8'时段选择符,用来指定所需使用的描述符项,此时是指gdt中的代码段描述符。'0'是描述符项指定的代码段中的偏移值。
6.3.3.2BIOS视频中断0x10
这里说明上面程序中用到的ROM BIOS中视频中断调用的几个字功能。
6.3.3.3硬件基本参数表("INT 0x41")
中断向量表中,int 0x41的中断向量位置(4*0x41=0x0000:0x0104)存放的并不是中断程序的地址,而是第一个硬盘的基本参数表。对于100%兼容的BIOS来说,这里存放着硬盘参数表阵列的首地址F000h:E401h。第二个硬盘的基本参数表入口地址存于int 0x46中断向量位置处。
6.3.3.4A20地址线问题
1981年8月,IBM公司最初推出的个人计算机IBM PC使用的CPU是Intel8088。在该微机中地址线只有20根(A0-A19)。在当时内存RAM只有几百KB或不到1MB时,20根地址线已足够来寻址这些内存。其所能寻址的最高地址是0xffff:0xffff,也即0x10ffef。对于超出0x100000(1MB)的寻址地址将默认地环绕到0x0ffef。由IBM公司于1985年引入AT机时,使用的是Intel 80286CPU,具有24根地址线,最高可寻址16MB,并且有一个与8088完全兼容的实模式运行方式。然而,在寻址值超过1MB时它却不能像8088那样实现地址寻址的环绕。但是当时已经有一些程序是利用这种地址环绕机制进行工作的。为了实现完全的兼容性,IBM公司发明了使用一个开关来开启或禁止0x100000地址比特位。由于在当时的8042键盘控制器上恰好有空闲的端口引脚(输出端口P2,引脚P21),于是便使用了该引脚来作为与门控制这个地址比特位。该信号即被称为A20。如果它为零,则比特20及以上地址都被清楚。从而实现了兼容性。
由于在机器启动时,默认条件下,A20地址线是禁止的,所以操作系统必须使用适当的方法来开启它。但是由于各种兼容机所使用的芯片集不同,要做到这一点却是非常的麻烦。因此通常要在集中控制方法中选择。
对A20信号线进行控制的常用方法是通过设置键盘控制器的端口值。这里的setup.s程序(138-144行)即使用了这种典型的控制方式。对于其他一些兼容微机还可以使用其他方式来做到对A20线的控制。
有些操作系统将A20的开启和禁止作为实模式和保护运行模式之间进行转换的标准过程中的一部分。由于键盘的控制器速度很慢,因此就不能使用键盘控制器对A20线进行操作。为此引进了一个A20快速门选项(Fast Gate A20),它使用I/O端口0x92来处理A20信号线,避免了使用慢速的键盘控制器操作方式。对于不含键盘控制器的系统就只能使用0x92端口来控制,但是该端口也有可能被其他兼容微机上的设备(如显示芯片)所使用,从而造成系统错误的操作。
还有一种方式是通过读0xee端口来开启A20信号线,写该端口则会禁止A20信号线。
6.3.3.58259A中断控制器的编程方法
在第2章中我们已经概要介绍了中断机制的基本原理和PC/AT兼容微机中使用的硬件中断子系统。这里我们首先介绍8259A芯片的工作原理,然后详细说明8259A芯片的编程方法以及Linux内核对其设置的工作方式。
!8259A芯片工作原理
前面已经说过,在PC/AT系列兼容机中使用了级联的两篇8259A可编程控制器(PIC)芯片,共可管理15级中断向量,参见图2-20所示。其中从芯片的INT引脚连接到主芯片的IR2引脚上。主8259A芯片的端口基地址是0x20,从芯片是0xA0。一个8259A芯片的逻辑图见图6-7所示。