bootsect.S
,系统引导程序,一般不超过
512
字节。
在
PC
系统结构中,线性地址
0xA0000
以上,即
640K
以上用于图形接口卡和
BIOS
自身,
640K
以下为系统的基本内存。如果配置更多的内存,则
0x100000
,即
1MB
处开始称为高内存。当
BIOS
引导一个系统时,总是把引导扇区读入到基本内存地址为
0x7c00
的地方,然后跳转到此执行引导扇区的代码。这段代码将自身搬运到
0x90000
处,并跳转到那继续执行,然后通过
BIOS
提供的读磁盘调用“
int 0x13”从磁盘上读入setup
和内核映像。其中
setup
的映像读入到
0x90200
处,然后跳转到
setup
的代码中。
从
0x90000
到
0xA0000
一共
64K
,
bootsect
仅占
512
字节,所以
setup
大小理论上可到
63.5KB
。
在
Linux2.4
版本以前,在最前面的
512
字节里保护了一个
mini “boot loader”
,只要拷贝启动代码运行就可从软盘启动;但在
2.6
版本中不再保护这样的
”boot loader”
,所以必须在第一个磁盘分区上存储一个合适的
boot loader
才能从软盘启动,软盘、硬盘和光驱启动都是一样的过程。
setup
进行映像的解压缩,从
BIOS
收集一些数据,在控制台显示一些信息。
基本内存中开头一部分空间是保留给
BIOS
自己用的,另一方面对于
Linux
内核的引导也需要保留一些运行空间,一共保存了
64K
。基本内存中用于内核映像的就是
8*64K=512K
,其中顶端留
4K
用于引导命令行及从
BIOS
获取需要传递给内核的数据。内核映像一般都经过压缩,压缩后的映像和引导扇区及辅助引导程序的映像拼接在一起,成为内核的引导映像。大小不超过
508K
的映像称为小映像
zImage
,早期版本放在
0x10000
位置处,否则称为大内核
bzImage
,放在
0x100000
位置处。
CPU
在
bootsect
时处于
16
位实地址模式,然后在
setup
的执行过程中转入
32
位保护模式。
Setup
从
BIOS
中读取系统数据(内存大小、显卡模式、磁盘等参数),将数据保存在
0x90000-0x901FF
,覆盖了
bootsect
的内容。设置
32
位运行方式:加载中断描述表寄存器
IDTR
、全局描述表寄存器
GDTR
;临时设置
IDT
表和
GDT
表,并在
GDT
表中设置内核代码段和数据段的描述符,在
Head.S
中会根据内核的需要重新设置这些描述符表;开启
A20
地址线;重新设置两个中断控制器
8259A
,将硬件中断号重新设置为
0x20
和
0x2f
;最后设置
CPU
的控制寄存器
CR0
(机器状态字)的保护模式比特(
PE
)位,从而进入
32
位保护模式运行;然后跳转到
head.S
中的
startup_32
执行。
对于小内核映像放在
0x10000
处,
Setup
会把
system
从
0x10000
移到
0x0000
开始处。对于大内核映像,
vmlinux
中普通内核代码被编译成以
PAGE_OFFSET+1MB
为起始地址,在
Head.S
中初始化代码把虚拟地址减去
PAGE_OFFSET
就能得到以
1MB
为起始位置的物理地址,这也正是内核映像在物理内存中的存放位置。
Head.S
中的
startup_32
主要用于开启页面单元。初始化工作在编译过程中开始进行,它先定义一个称为
swapper_pg_dir
的数组,使用链接器指示在地址
0x00101000
。然后分别为两个页面
pg0
和
pg1
创建页表项。第一组指向
pg0
和
pg1
的指针放在能覆盖
1
~
9MB
内存的位置,第二组指针放在
PAGE_OFFSET+1MB
的位置。一旦开始页机制,在上述页表和页表项指针建立后可以保证,在内核映像中不论是采用物理地址还是虚拟地址,都可以进行正确的页面映射。内核其他部分的页表初始化在
paging_init()
中完成。映射建立后,通过设置
cr0
寄存器中的某位开启页面映射,然后通过一个跳转指令保证指令指针的正确性。
1.Bootsect启动过程:
假设用
LILO
启动,启动时用户可以选择启动哪个操作系统。
LILO
将
boot loader
分为两部分,一部分放到启动分区的第一个扇区;
1)
BIOS
将
MBR
或启动分区的第一个扇区的启动部分加载到地址
0x00007c00
处;
2)
该程序将自身移到
0x00096a00
,建立实模式栈
(
从
0x00098000
到
0x000969ff)
,将
LILO
的第二部分加载到
0x00096c00
处,然后跳转到此执行;
3)
然后第二部分程序从磁盘读取一个可启动的操作系统列表让用户选择,最后用户选择每个
OS
后,
boot loader
可以拷贝不启动分区或者之间拷贝内核映像到
RAM
中去;
4)
加载
Linux
内核映像时,
LILO boot loader
首先调用
BIOS
例程显示
”Loading …”
信息;
5)
调用
BIOS
例程加载内核映像的初始化部分到
RAM
上,内核映像的前
512
字节放在
0x00090000
位置,
setup()
函数代码放在
0x00090200
位置;
6)
接着调用
BIOS
例程装载内核映像的其余部分,映像可能放在低地址
0x00010000(
使用
make zImage
编译的小内核映像
)
或者高地址
0x00100000
(使用
make bzImage
编译的大内核映像)。
7)
然后跳至刚刚
setup
部分。
2.Setup.S分析
setup()
汇编函数被连接器放在内核映像文件中的
0x200
偏移处。
Setup
函数必须初始化计算机中的硬件设备并为内核程序的执行建立环境。
1)
在
ACPI
兼容的系统中,调用
BIOS
例程建立描述系统物理内存布局的表。在早期系统中,它调用
BIOS
例程返回系统可以的
RAM
大小;
2)
设置键盘的重复延迟和速率;
3)
初始化显卡;
4)
检测
IBM MCA
总线、
PS/2
鼠标设备、
APM BIOS
支持等;
5)
如果
BIOS
支持
Enhanced Disk Drive Services (EDD)
,将调用正确的
BIOS
例程建立描述系统可用硬盘的表;
6)
如果内核加载在低
RAM
地址
0x00010000
,则把它移动到
0x00001000
处;如果映像加载在高内存
1M
位置,则不动;
7)
启动位于
8042
键盘控制器的
A20 pin
。
8)
建立一个中断描述表
IDT
和全局描述表
GDT
表;
9)
如果有的话,重启
FPU
单元;
10)
对可编程中断控制器进行重新编程,屏蔽所以中断,级连
PIC
的
IRQ2
不需要;
11)
设置
CR0
状态寄存器的
PE
位使
CPU
从实模式切换到保护模式,
PG
位清
0
,禁止分页功能;
12)
跳转到
startup_32()
汇编函数
, jmpi 0x100000, __BOOT_CS
,终于进入内核
Head.S
;
3.Head.S分析
有两个不同的
startup_32()
函数,一个在arch/i386/boot/compressed/head.S
文件中,
setup
结束后,该函数被放在
0x00001000
或者
0x00100000
位置,该函数主要操作:
1)
首先初始化段寄存器和临时堆栈;
2)
清除
eflags
寄存器的所有位;
3)
将
_edata
和
_end
区间的所有内核未初始化区填充
0
;
4)
调用
decompress_kernel( )
函数解压内核映像。首先显示
"Uncompressing Linux..."
信息,解压完成后显示
"OK, booting the kernel."
。内核解压后,如果时低地址载入,则放在
0x00100000
位置;否则解压后的映像先放在压缩映像后的临时缓存里,最后解压后的映像被放置到物理位置
0x00100000
处;
5)
跳转到
0x00100000
物理内存处执行;
解压后的映像开始于
arch/i386/kernel/head.S
文件中的
startup_32()
函数,因为通过物理地址的跳转执行该函数的,所以相同的函数名并没有什么问题。该函数未
Linux
第一个进程建立执行环境,操作如下:
1)
初始化
ds,es,fs,gs
段寄存器的最终值;
2)
用
0
填充内核
bss
段;
3)
初始化
swapper_pg_dir
数组和
pg0
包含的临时内核页表:
l
将
swapper_pg_dir
(
0x1000)
和
pg0(0x2000)
清空,
swapper_pg_dir
作为整个系统的页目录;
l
将
pg0
作为第一个页表,将其地址赋到
swapper_pg_dir
的第一个
32
位字中。
l
同时将该页表项也赋给
swapper_pg_dir
的第
3072
个入口,表示虚拟地址
0xc0000000
也指向
pg0
。
l
将
pg0
这个页表填满指向内存前
4M
。
l
在
cr3
寄存器中存放
PGD
的地址,并设置
cr0
寄存器中的
PG
位,启用分页支持。
4)
建立进程
0idle
进程的内核模式的堆栈;
5)
再次清除
eflags
寄存器的所有位;
6)
调用
setup_idt()
用非空的中断处理函数填充
IDT
表;
7)
将从
BIOS
获取的系统参数传递到操作系统的第一个页面帧;
8)
识别处理器的模式;
9)
将
GDT
和
IDT
表的地址加载到
gdtr
和
idtr
寄存器中;
10)
跳转到
start_kernel
函数,这个函数是第一个
C
编制的函数,内核又有了一个新的开始。
4.start_kernel()分析:
1)
调度器初始化,调用
sched_init();
2)
调用
build_all_zonelists
函数初始化内存区;
3)
调用
page_alloc_init()
和
mem_init()
初始化伙伴系统分配器;
4)
调用
trap_init()
和
init_IRQ()
对中断控制表
IDT
进行最后的初始化;
5)
调用
softirq_init()
初始化
TASKLET_SOFTIRQ
和
HI_SOFTIRQ
;
6)
Time_init()
对系统日期和时间进行初始化;
7)
调用
kmem_cache_init()
初始化
slab
分配器;
8)
调用
calibrate_delay()
计算
CPU
时钟频率;
通过调用kernel_thread()启动进程1init进程的内核线程,然后该线程再创建其他的内核线程执行/sbin/init程序。