每个域的具体细节可参考boot.txt文档。
BIOS把Boot Loader加载到0x7C00的地方并跳转到这里继续执行之后,BootLoader就会把实模式代码setup加载到0x07C00之上的某个地址上,其中setup的前512个字节是boot sector(引导扇区),现在这个引导扇区的作用并不是用来引导系统,而是为了兼容及传递一些参数。之后Boot Loader跳转到setup的入口点,入口点为_start例程(根据arch/x86/boot/setup.ld可知)。
注意,bzImage由setup和vmlinux两部分组成,setup是实模式下的代码,vmlinux是保护模式下的代码。
实模式设置(setup)阶段用于体系结构相关的硬件初始化工作,涉及的文件有arch/x86/boot/header.S、链接脚本setup.ld、arch/x86/boot/main.c。header.S第一部分定义了bstext、.bsdata、.header这3个节,共同构成了vmlinuz的第一个512字节(即引导扇区的内容)。常量BOOTSEG和SYSSEG定义了引导扇区和内核的载入地址。下面是header.S的代码:
BOOTSEG = 0x07C0 /* 引导扇区的原始地址 */
SYSSEG = 0x1000 /* 历史的载入地址>>4 */
#ifndef SVGA_MODE
#define SVGA_MODE ASK_VGA
#endif
#ifndef RAMDISK
#define RAMDISK 0
#endif
#ifndef ROOT_RDONLY
#define ROOT_RDONLY 1
#endif
.code16
.section ".bstext", "ax"
.global bootsect_start
bootsect_start:
# 使开始地址正常化
ljmp $BOOTSEG, $start2
start2:
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp
sti
cld
movw $bugger_off_msg, %si
msg_loop:
lodsb
andb %al, %al
jz bs_die
movb $0xe, %ah
movw $7, %bx
int $0x10
jmp msg_loop
bs_die:
# 允许用户按一个键,然后重启
xorw %ax, %ax
int $0x16
int $0x19
# 0x19中断绝不会返回,无论它做什么
# 调用BIOS复位代码,便CPU工作在实模式下
ljmp $0xf000,$0xfff0
.section ".bsdata", "a"
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
# 下面设置内核的一些属性,setup需要。这是header的第一部分,来自以前的boot sector
.section ".header", "a"
.globl hdr
hdr:
setup_sects: .byte 0 /* 被build.c填充 */
root_flags: .word ROOT_RDONLY
syssize: .long 0 /* 被build.c填充 */
ram_size: .word 0 /* 已过时 */
vid_mode: .word SVGA_MODE
root_dev: .word 0 /* 被build.c填充 */
boot_flag: .word 0xAA55
# 偏移512处,setup的入口点
.globl _start
_start:
# Explicitly enter this as bytes, or the assembler
# tries to generate a 3-byte jump here, which causes
# everything else to push off to the wrong offset.
.byte 0xeb # short (2-byte) jump
.byte start_of_setup-1f
1:
# header的第二部分,来自以前的setup.S:设置头部header,包括大量的bootloader参数,如header版本、内核版本字符串指针、bootloader类型、
# 内核装载时的很多标志、堆栈尾部地址指针、内核命令行地址指针和大小、32位保护模式入口地址、ramdisk地址和大小等
code32_start: # 这里对32位的代码,装载器可以设置可设置一个不同的入口地址
.long 0x100000 # 0x100000 = 为大内核的默认入口地址(保护模式)
# ............ (省略)
# End of setup header #####################################################
.section ".entrytext", "ax"
start_of_setup:
#ifdef SAFE_RESET_DISK_CONTROLLER
# 重置磁盘控制器
movw $0x0000, %ax # 重置磁盘控制器
movb $0x80, %dl # 所有的的磁盘控制器All disks
int $0x13
#endif
# ............(省略)
# 让%ss无效,创建一个新的栈
movw $_end, %dx
testb $CAN_USE_HEAP, loadflags
jz 1f
movw heap_end_ptr, %dx
1: addw $STACK_SIZE, %dx
jnc 2f
xorw %dx, %dx # Prevent wraparound
2: # 现在%dx应该指向我们栈空间的尾部
andw $~3, %dx # dword对齐
jnz 3f
movw $0xfffc, %dx # 确保不是0
3: movw %ax, %ss
movzwl %dx, %esp # 清除%esp的上半部分
sti # 现在我们应该有一个工作空间
# 我们将进入%cs=%ds+0x20,设置好%cs
pushw %ds
pushw $6f
lretw
6:
# 在setup终止时检查签名
cmpl $0x5a5aaa55, setup_sig
jne setup_bad
# 对BSS(Block Started by Symbol)清零
movw $__bss_start, %di
movw $_end+3, %cx
xorl %eax, %eax
subw %di, %cx
shrw $2, %cx
rep; stosl
# 跳转到C代码(不会返回)
calll main
# ............(省略)
由setup.ld中的ENTRY(_start)可知,_start汇编例程是bzImage内核映像开始执行的入口点,即引导扇区之后的开始处(偏移512字节处),它会准备大量的bootloader参数。最后的call main跳转到arch/x86/boot/main.c:main()函数处执行,这就是众所周知的main函数,它们都工作在实模式下。main函数先调用copy_boot_params函数把位于第一个扇区的参数复制到boot_params变量中,boot_params位于setup的数据段,然后调用链为arch/x86/boot/pm.c:go_to_protected_mode(void) --->arch/x86/boot/pmjump.S:protected_mode_jump()。
实模式的protected_mode_jump执行后,跳出了bzImage的第一部分,BootLoader默认把第二部分放在0x100000处,这个入口处是startup_32,先执行arch/x86/boot/compressed/head_32.S中的startup_32(保护模式下的入口函数),然后执行arch/x86/kernel/head_32.S中的startup_32(32位内核的入口函数),这里会拷贝boot_params以及boot_command_line, 初始化页表,开启分页机制。
startup_32()函数会调用head32.c:i386_start_kernel()函数,它会调用init/main.c:start_kernel()函数,这是Linux内核的启动函数。init/main.c文件是整个Linux内核的中央联结点。每种体系结构都会执行一些底层设置函数,然后执行名为start_kernel的函数(在init/main.c中可以找到这个函数)。可以认为main.c是内核的“粘合剂(glue)”,之前执行的代码都是各种体系结构相关的代码,一旦到达start_kernel(),就与体系结构无关了。
start_kernel()会调用一系列初始化函数来设置中断,执行进一步的内存配置,解析内核命令行参数。然后调用fs/dcache.c:vfs_caches_init()--->fs/namespace.c:mnt_init()创建基于内存的rootfs文件系统(是一个虚拟的内存文件系统,称为VFS),这是系统初始化时的根结点,即"/"结点,后面VFS会指向真实的文件系统。注意在Linux系统中,目录结构与Windows上有较大的不同。系统中只有一个根目录,路径是“/”,而其它的分区只是挂载在根目录中的一个文件夹内,如“/proc”和“/sys”等,这里的“/”就是Linux中的根目录。
下面是mnt_init()的代码:
void __init mnt_init(void)
{
unsigned u;
int err;
init_rwsem(&namespace_sem);
mnt_cache = kmem_cache_create("mnt_cache", sizeof(struct vfsmount),
0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
mount_hashtable = (struct list_head *)__get_free_page(GFP_ATOMIC);
if (!mount_hashtable)
panic("Failed to allocate mount hash table\n");
printk("Mount-cache hash table entries: %lu\n", HASH_SIZE);
for (u = 0; u < HASH_SIZE; u++)
INIT_LIST_HEAD(&mount_hashtable[u]);
err = sysfs_init();
if (err)
printk(KERN_WARNING "%s: sysfs_init error: %d\n",
__func__, err);
fs_kobj = kobject_create_and_add("fs", NULL);
if (!fs_kobj)
printk(KERN_WARNING "%s: kobj create error\n", __func__);
init_rootfs();
init_mount_tree();
}
这里fs/ramfs/inode.c:init_rootfs()会调用fs/filesystems.c:register_filesystem()注册rootfs。然后fs/namespace.c:init_mount_tree()调用fs/super.c:do_kern_mount()在内核中挂载rootfs,调用fs/fs_struct.c:set_fs_root()将当前的rootfs文件系统配置为根文件系统。