系统初启

首先很惭愧的坦白《读核札记》的第一篇就大部分是抄袭他人(XIAOMAN)的
因为系统的初起一直是一个麻烦而头疼的问题,不同的体系结构
会有较大的不同。X86从硬件启动,读入引导扇区,执行引导程序,
从实模式开始再转换到保护模式这个复杂的过程其实与操作系统本身
的运行机制关系并不大,但忽略过去又无法给LINUX内核一个完整的
过程,所以我大动剪刀浆糊,但会把精力主要集中在LINUX内核本身,
希望得到大家的谅解。

(以核心2.0.36为主)

系统引导:

涉及的文件
./arch/$ARCH/boot/bootsect.s
./arch/$ARCH/boot/setup.s

bootsect.S
 这个程序是linux kernel的第一个程序,包括了linux自己的bootstrap程序,
但是在说明这个程序前,必须先说明一般IBM PC开机时的动作(此处的开机是指
"打开PC的电源"):

  一般PC在电源一开时,是由内存中地址FFFF:0000开始执行(这个地址一定
在ROM BIOS中,ROM BIOS一般是在FEOOOh到FFFFFh中),而此处的内容则是一个
jump指令,jump到另一个位於ROM BIOS中的位置,开始执行一系列的动作,包
括了检查RAM,keyboard,显示器,软硬磁盘等等,这些动作是由系统测试代码
(system test code)来执行的,随着制作BIOS厂商的不同而会有些许差异,但都
是大同小异,读者可自行观察自家机器开机时,萤幕上所显示的检查讯息。

  紧接着系统测试码之后,控制权会转移给ROM中的启动程序
(ROM bootstrap routine),这个程序会将磁盘上的第零轨第零扇区读入
内存中(这就是一般所谓的boot sector,如果你曾接触过电脑病
毒,就大概听过它的大名),至於被读到内存的哪里呢? --绝对
位置07C0:0000(即07C00h处),这是IBM系列PC的特性。而位在linux开机
磁盘的boot sector上的正是linux的bootsect程序,也就是说,bootsect是
第一个被读入内存中并执行的程序。现在,我们可以开始来
看看到底bootsect做了什么。

第一步
 首先,bootsect将它"自己"从被ROM BIOS载入的绝对地址0x7C00处搬到
0x90000处,然后利用一个jmpi(jump indirectly)的指令,跳到新位置的
jmpi的下一行去执行,

第二步
 接着,将其他segment registers包括DS,ES,SS都指向0x9000这个位置,
与CS看齐。另外将SP及DX指向一任意位移地址( offset ),这个地址等一下
会用来存放磁盘参数表(disk para- meter table )

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

第四步
 完成重置磁盘控制器之后,bootsect就从磁盘上读入紧邻着bootsect的setup
程序,也就是setup.S,此读入动作是利用BIOS中断服务int 13h的第2号功能。
setup的image将会读入至程序所指定的内存绝对地址0x90200处,也就是在内存
中紧邻着bootsect 所在的位置。待setup的image读入内存后,利用BIOS中断服
务int 13h的第8号功能读取目前磁盘的参数。


第五步
 再来,就要读入真正linux的kernel了,也就是你可以在linux的根目录下看
到的"vmlinuz" 。在读入前,将会先呼叫BIOS中断服务int 10h 的第3号功能,
读取游标位置,之后再呼叫BIOS 中断服务int 10h的第13h号功能,在萤幕上输
出字串"Loading",这个字串在boot linux时都会首先被看到,相信大家应该觉
得很眼熟吧。

第六步
 接下来做的事是检查root device,之后就仿照一开始的方法,利用indirect
jump 跳至刚刚已读入的setup部份

第七步
setup.S完成在实模式下版本检查,并将硬盘,鼠标,内存参数写入到 INITSEG
中,并负责进入保护模式。

第八步
是我们详细讨论的开始,即操作系统的初始化。

--
※ 来源:.武汉白云黄鹤站 bbs.whnet.edu.cn.[FROM: 202.112.20.1]
--

--


内核开始的地方(一)

我们主要讨论的文件是 arch/i386/kernel/head.S
首先我们要保证这个程序的的确确是内核的开始,因为setup.S最后的
为一条转跳指令,跳到内核第一条指令并开始执行。指令中指向的是
内存中的绝对地址,我们无法依此判断转跳到了head.S。

但是我们可以通过Makefile简单的确定head.S位于内核的前端。

在arch/i386 的 Makefile 中定义了
HEAD := arch/i386/kernel/head.o

而在linux总的Makefile中由这样的语句
include arch/$(ARCH)/Makefile
说明HEAD定义在该文件中有效

然后由如下语句:
vmlinux: $(CONFIGURATION) init/main.o init/version.o linuxsubdirs
$(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o \
$(ARCHIVES) \
$(FILESYSTEMS) \
$(DRIVERS) \
$(LIBS) -o vmlinux
$(NM) vmlinux | grep -v '\(compiled\)\|\(\.o$$\)\|\( a \)' | sort > System.map

从这个依赖关系我们可以获得大量的信息

1>$(HEAD)即head.o的确第一个被连接到核心中

2>所有内核中支持的文件系统全部编译到$(FILESYSTEMS)即fs/filesystems.a中
所有内核中支持的网络协议全部编译到net.a中
所有内核中支持的SCSI驱动全部编译到scsi.a中
...................
原来内核也不过是一堆库文件和目标文件的集合罢了,有兴趣对内核减肥的同学,
可以好好比较一下看究竟是那个部分占用了空间。

3>System.map中包含了所有的内核输出的函数,我们在编写内核模块的时候
可以调用的系统函数大概就这些了。


好了,消除了心中的疑问,我们可以仔细分析head.s了。


 

内核开始的地方(二)

Head.S分析

1 首先将ds,es,fs,gs指向系统数据段KERNEL_DS
KERNEL_DS 在asm/segment.h中定义,表示全局描述符表中
中的第三项。
注意:该此时生效的全局描述符表并不是在head.s中定义的
而仍然是在setup.S中定义的。

2 数据段全部清空。

3 setup_idt为一段子程序,将中断向量表全部指向ignore_int函数
该函数打印出:unknown interrupt
当然这样的中断处理函数什么也干不了。

4 察看数据线A20是否有效,否则循环等待。
地址线A20是x86的历史遗留问题,决定是否能访问1M以上内存。

5 拷贝启动参数到0x5000页的前半页,而将setup.s取出的bios参数
放到后半页。

6 检查CPU类型
@#$#%$^*@^?(^%#$%!#!@?谁知道干了什么?

7 初始化页表,只初始化最初几页。

1>将swapper_pg_dir(0x2000)和pg0(0x3000)清空
swapper_pg_dir作为整个系统的页目录

2>将pg0作为第一个页表,将其地址赋到swapper_pg_dir的第一个32
位字中。

3>同时将该页表项也赋给swapper_pg_dir的第3072个入口,表示虚拟地址
0xc0000000也指向pg0。

4>将pg0这个页表填满指向内存前4M

5>进入分页方式
注意:以前虽然在在保护模式但没有启用分页。

--------------------
| swapper_pg_dir | -----------
| |-------| pg0 |----------内存前4M
| | -----------
| |
--------------------
8 装入新的gdt和ldt表。

9 刷新段寄存器ds,es,fs,gs

10 使用系统堆栈,即预留的0x6000页面

11 执行start_kernel函数,这个函数是第一个C编制的
函数,内核又有了一个新的开始。