首先一个操作系统的内核的启动由几部分完成:
1. 引导内核: 首先要在第一个扇区编写小于512字节的启动代码,或者使用现存的bootloader,如: LILO, GRUB等。
2. 加载内核: 将内核放在物理内存的某个物理地址上。
3. 执行内核: 指令指针转移到内核中与平台相关(如: x86, ARM)的机器码的位置,并初始化基本的功能,平台相关的代码大多用汇编编写。随后转移到平台无关的代码,完成系统的初始化,最后切换到正常的运转模式(如: IA-32下一般需要激活分页功能,并且打开保护模式的标志位跳转到保护模式)。
首先看第一阶段linux内核的实现过程(以下将linux2.6.11内核统称为linux内核,各版本代码不一致):
看一下当前根目录下的Makefile,有以下语句:
-
include $(srctree)/arch/$(ARCH)/Makefile
$(srctree)指的就是当前的根目录,ARCH可以由我们在编译内核的时候指定,如果不指定默认为当前系统的体系结构:
-
SUBARCH := $(shell uname -m | sed -e s/i.86/i386/ -e s/sun4u/sparc64/ \
-
-e s/arm.*/arm/ -e s/sa110/arm/ \
-
-e s/s390x/s390/ -e s/parisc64/parisc/ )
由SUBARCH这个变量来判断当前机器的体系结构。
为了方便默认为i386,具体数值可以把命令Copy一下自己在shell执行一下。
来看一下bootsect.S,该代码是较早的linux版本的引导代码。和基本的引导代码一样,首先跳转到0x7c00,随后执行代码,添加0xaa55的标记,最后让代码填充到512字节。
但是在目前的版本抛弃了该执行过程,看代码(arch/$(ARCH)/boot/bootsect.S):
-
movw $bugger_off_msg, %si
-
-
msg_loop:
-
lodsb
-
andb %al, %al
-
jz die
-
movb $0xe, %ah
-
movw $7, %bx
-
int $0x10
-
jmp msg_loop
-
-
die:
-
# Allow the user to press a key, then reboot
-
xorw %ax, %ax
-
int $0x16
-
int $0x19
-
-
# int 0x19 should never return. In case it does anyway,
-
# invoke the BIOS reset code...
-
ljmp $0xf000,$0xfff0
-
-
-
bugger_off_msg:
-
.ascii "Direct booting from floppy is no longer supported.\r\n"
-
.ascii "Please use a boot loader program instead.\r\n"
-
.ascii "\n"
-
.ascii "Remove disk and press any key to reboot . . .\r\n"
-
.byte 0
上述代码执行一个循环显示bugger_off_msg告诉用户软盘启动不是长期支持的,请使用bootloader。die函数使用BIOS中断(可以在书中查找,《intel汇编语言程序设计》。或者查看标准的Phoenix BIOS,如果没有该文献和书籍,可以联系我) 等待一个按键重新启动。
很明显Linux内核使用bootloader启动。
继续分析代码文件(arch/$(ARCH)/boot/setup.S[使用LILO的启动代码文件]):
-
/* Signature words to ensure LILO loaded us right */
-
#define SIG1 0xAA55
-
#define SIG2 0x5A5A
-
-
INITSEG = DEF_INITSEG # 0x9000, we move boot here, out of the way
-
SYSSEG = DEF_SYSSEG # 0x1000, system loaded at 0x10000 (65536).
-
SETUPSEG = DEF_SETUPSEG # 0x9020, this is the current segment
-
# ... and the former contents of CS
注释写到用特征代码来确定LILO加载是正确的,说明SIG1和SIG2可能用在文件尾部来判断是否加载成功,与软盘启动0xaa55同样道理。(如果有LILO源代码或者文档请发给我,感谢!)。
其他的代码就是一些启动代码和从磁盘读取内核的代码,例如: 使能a20地址线,读硬盘,加载GDT,LDT,开启分页或者PAE等功能然后跳转到保护模式进入32位代码等等。
本文不叙述(太多了!), 其他文章进行详细分析0-0。
略过其他代码,但还是要看一段:
-
.byte 0x66, 0xea # prefix + jmpi-opcode
-
code32: .long 0x1000 # will be set to 0x100000
-
# for big kernels
0x66,0xea代表jmpi指令和jmpi指令前缀,加0x66指令前缀是因为我们没有重载CS寄存器,所以当前还处于实模式,没有办法跳转到0x100000的地址位置,使用0x66后,CPU将会给我们一个48位的远指针。后面跟着0x1000也就表示这段代码实际表示:
jmpi 0x1000或者0x100000, __BOOT_CS,code32标签的地址定义为如下代码:
-
code32_start:
-
# start address for 32-bit code.
-
#ifndef __BIG_KERNEL__
-
.long 0x1000 # 0x1000 = default for zImage
-
#else
-
.long 0x100000 # 0x100000 = default for big kernel
-
#endif
也就是说如果定义了__BIG_KERNEL__(bzImage)就使用0x100000作为跳转地址,否则就使用0x1000(zImage)。
跳转到startup_32之后做一些初始化工作: 清理BSS等。随后解压内核调用decompress_kernel函数(boot/compress/misc.c),并且跳转到解压后的内核代码位置(arch/$(ARCH)/kernel/head.S)该代码做一些最后的初始化功能:
1. 激活分页
2. 清理bss(用0字节填充__bss_start和_bss_end之间的内容)
3. 初始化中断描述符表
4. 检测处理器类型
最后调用start_kernel函数。内核就真正的开始启动了。
EMAIL回复会比较及时
深入Linux内核架构(中文版) Wolfgang Manuerer著 郭旭译
阅读(4715) | 评论(0) | 转发(0) |