0
引言
从嵌入式系统到超级服务站,
Linux
已获得广泛的应用。
Linux
是一个完整通用的
Unix
类分布式操作系统,它的结构紧凑、功能强、效率高、可移植性好且在
Internet
上可自由取用。
Linux
和
Unix
操作系统一样,操作系统的主要功能集中在内核,内核中包含进程管理、文件管理、设备管理和网络管理等部分。本文深入探讨
Linux
操作系统内核的启动过程,希望对
Linux
学习和使用者有所启迪。
1
内核结构及平台相关性
本文分析的内核版本为
2.6.9
。当我们使用
tar
命令将
linux-2.6.9.tar.bz2
解开时,内核源代码被放到了
linux-2.6.9/
目录中。
Linux
内核各功能文件分别存放在
linux-2.6.9/
目录下的相应子目录中。
Linux
操作系统可以工作在多种不同硬件平台上,如
80x86CPU
系列
(80386
以上
)
、
SUN sparc64
和
arm26
等。为了让
Linux
体现优良的可移植性,
Linux
内核代码针对不同的硬件平台包含有对应的启动和初始化程序。这些程序处于
arch/
子目录中。用户完全可以根据自己的需要,从内核代码中各取所需,即时编译和更换系统内核,这也是
Linux
操作系统获得世界各地网络爱好者普遍支持的主要原因。鉴于绝大部分
Linux
应用于
Intel 80x86
系列平台,所以本文也仅限对
Linux
在
80x86
系列平台的启动过程进行分析。本文所要探讨的启动程序位于
arch/i386/boot/
目录,系统的启动过程主要由
bootsect.s
、
setup.s
和
head.s
等
3
个汇编程序完成。
2
系统启动流程
当
PC
的电源打开后,
80x86
结构的
CPU
将自动进入实模式,并从地址
0XFFFF0
开始自动执行程序代码,这个地址通常是
ROM-BIOS
中的地址。
PC
机的
BIOS
将执行某些系统的检测,如测试内存等,并在物理地址
0
处开始初始化中断向量。此后,它将可启动设备的第
1
个扇区
(
磁盘引导扇区,
512
字节
)
读入内存绝对地址
0x7C00
处,并把控制权交给这段代码。
Linux
的这段代码
(boot/bootsect.s)
是用
8086
汇编语言编写,它首先把自己举到内存地址
0x90000
,并跳转到这里执行。然后把启动设备中后
2KB
代码
(boot/setup.s)
读入到内存
0x90200
处,而内核的其它部分
(system
模块
)
则被读入到从地址
0x10000
开始处。随后
setup.s
将把
system
模块移动到内存起始处,这样
system
模块中代码的地址就等于实际的物理地址,便于对内核代码的操作。
Linux
内核编译后生成的内核映像
(system
模块
)
最大只能为
512 KB
,超出这个大小将无法装入。这虽可以满足一般内核要求,但在特殊情况下内核也会大于
512 K
,这时可用两种办法:①把内核映像压缩
(ZIP
格式
)
,系统进入操作模式后再展开;②把某些非基本的具有独立功能的部分编译成模块,在系统启动后或启动过程中插入。待系统模块
(system)
头部运行结束,所有
32
位运行方式的设置启动都完成,
IDT
、
GDT
以及
LDT
被加载,处理器和协处理器被确认,分页工作也设置好了。最终调用
init/main.c
中的
main()
程序进入系统的初始化过程,至此系统获得正常启动。
3
启动过程分析
主要对
bootsect.s
、
setup.s
和
head.s
的工作机理做了较为详细的阐述。
3.1 bootsect
模块分析
bootsect.s
代码是磁盘引导块程序,驻留在引导盘的引导扇区
(0
磁道,
0
磁头,第
1
扇区
)
。在
PC
加电
ROM BIOS
自检后,
bootsect.s
由
BIOS
自动加载到内存
0x7C00
处,然后将自己移到内存
0x90000
处。在
bootsect.s
的移动过程,其中
#BOOTSEG
为
0x7C00
,
#INITSEG
为
0x9000
。接下来,程序利用
BIOS
中断,
INT 0x13
将
setup
模块从磁盘第
2
个扇区开始读到
0x90200
开始处,共读
4
个扇区。如果读出错误,则
CF
标志置位,程序复位驱动器,并重试。加载
setup
模块后,程序利用中断取磁盘驱动器参数,并将中断返回的每磁道扇区数保存在变量
sectors
中。然后程序将
system
模块加载到内存
0x10000
处。加载
system
模块期间,显示“
Loading system
?
”信息。为了提高加载速度,只要可能,就每次加载整条磁道的数据。从磁盘读取一次数据后,程序就比较当前所读段是否就
是系统数据末端所处的段
(#ENDSEG)
,如果不是,就跳转至
ok1_read
标号处继续读数据。最后,程序向软驱控制卡的驱动端口
0x3f2
写
0
,关闭软驱电动机。程序运行
jmpi 0
,
SETUPSEG
,跳转到
0x9020:0000
处,
CPU
开始执行
setup
模块。
3.2 setup
模块分析
setup.s
首先利用
ROMBIOS
中断读取机器系统参数
(
光标位置、扩展内存数、硬盘参数表等
)
,并将这些数据保存到内存
0x90000
开始的位置
(
覆盖掉了
bootsect
程序
)
。这些参数将被内核中相关程序使用,例如设备驱动程序集中的
ttyio.c
。随后系统进入保护模式运行。
CPU
在实模式下运行,寻址一个内存地址主要是使用段基址和段内偏移值,段值被存放在段寄存器中;而在保护模式运行方式下,段寄存器中存放的是一个描述表中某项的索引值。索引值指定的描述符项中含有
需要寻址的内存段的基地址、段的最大长度值和段的访问级别等信息。和实模式下的寻址相比,段寄存器值换成了段描述符项索引。接下来,程序关闭中断,将
system
模块整体向内存低端移动
0x1000
。每次移动
0x8000
字,循环执行
8
次。然后程序执行
lidt idt_48
加载中断描述符表
(idt)
寄存器;执行
lgdt gdt_48
加载全局描述符表
(gdt)
寄存器。此时中断描述符表中只有一个空项
(
值全为
0)
。全局描述符表中有
3
个描述符项:第
1
项无用,但必须存在;第
2
项
(
索引值
0x08)
是系统代码段描述符,所定义的段基址为
0
,段中代码可被读和执行,段长为
8 M
;第
3
项
(
索引值
0x10)
是系统数据段描述符,所定义的段的基址为
0
,段中数据可读和可写,段限长为
8M
。最后,程序重置协处理器,对
8259
中断控制芯片编程,完成进入保护虚地址模式的所有准备工作。通过设置机器状态字
MSW(
第
0
号控制寄存器
CR0
的低
16
位
)
中的
PE
位使
CPU
进入保护模式,开始运行
system
模块中的
head.s(
指令
jmpi 0,8)
。注意,
CPU
已在保护模式下运行,
CS
置
8
表示请求特权级
0
,使用全局描述符表中的第
1
项
(
索引值
0x08)
。
3.3 head
模块分析
全局描述中的第
1
项表明
CPU
开始执行内存基址为
0x0000
处的代码,这里存放的是
system
模块的头部,即为
32
位汇编程序
kernel/head.s
。
head.s
采用的是
AT&T
汇编语言格式,并且需要使用
GNU
的
gas
和
gld
进行编译连接。
程序首先将
%ds
、
%es
、
%fs
和
%gs
置立即数
0x10
,这里的
0x10
并不是把地址
0x10
装入各个段寄存器,它是全局描述符表中的偏移值。然后程序设置系统新的中断描述符表
(lidt idt_drscr)
,新的
idt
处于
head.s
尾,共
256
项
(
每项
8B)
,并都指向
ignore_int
哑中断门,真正实用的中断门以后再安装。
程序重新设置系统全局描述符表
(lgdt gdt_descr)
,新的
GDT
在
head.s
末端,共
256
项。前
4
项分别是空项
(
不用
)
、代码段描述符、数据段描述符和系统段描述符,其中系统段描述符
Linux
没有派用处。后面还预留了
252
项的空间,用于放置所创建任务的局部描述符
(LDT)
和对应的任务状态段
TSS
的描述符。
然后程序设置页表并启动分页机制,
Linux
在
386
系列平台使用
2
级页表,即页表目录和页表。从基地址
0x1000
处开始填充页表,
0x0000
处将存放页表目录。当前代码的物理基地址已变为
0x1000(
指令
.org 0x1000)
,程序连续分配
4
个页表,每个页表大小为
4 K
。页表项为
4
个字节,包含页框地址、内存页信息
(
是否在内存、是否修改过、普通用户还是超级用户等
)
。程序紧接对
5
页内存
(1
页目录
+4
页页表
)
清零,开始设置页目录中的项。“
$pg0+7
”表示
0x00001007
是页目录表中的第
1
项。则第
1
个页表所在的地址为:
0x00001007&0xfffff000
;第
1
个页表的属性标志为:
0x00001007&00000fff
,表示该页存在,用户可读写。
xor%eax
,
%eax
;
movl %eax
,
%cr3
设置页目录基址寄存器
cr3
。
Movl%cr0
,
%eax
;
orl $0x80000000
,
%eax
;
movl%eax
,
%cr0
启动控制寄存器
cr0
的分页标志
(PG)
位。
至此
386
系列
CPU
的分段机制和分页机制均已启动,内核也置于最终的适当位置,与平台有关的启动过程基本完成。系统内核将进入与平台无关的内核数据结构初始化工作。采用的方法是,在
head.s
中将
main.c
程序的地址压入堆栈。这样,在设置分页处理
(setup_paging)
结束后执行“
ret
”返回指令时就会将
main.c
程序的地址弹出堆栈,并去执行
main.c
程序。
main.c
调用所有设置过程,如为设备申请
I/O
空间、其它页表的初始化、缓冲区初始化、网络初始化和设置多任务等,然后系统转移到用户模式,创建并启动初始进程
init
。这些初始化设置过程都与
Linux
的设备管理、文件管理、网络管理和
CPU
管理和内存管理有关。这样
Linux
操作系统完全启动。
4
结束语
Linux
是当前流行的一种操作系统,
Linux
有许多不同的版本,比较流行的有
Red Hat
、
Slackware
和
Debian
,各版本的
Linux
之间的区别不大,其内核都差不多。