Linux内核启动流程分为如下几个阶段:
- BIOS启动阶段
- 实模式setup阶段
- 保护模式 startup_32
- 内核启动start_kernel()
- 内核启动时的参数传递
下面我们一一进行分析。阅读整篇文章的同时,可以参考内核源代码文档 Documentation\i386\boot.txt。本篇文章基于Linux 2.6.24
BIOS启动阶段
传统BIOS程序存放在只读存储器中,非常不方便修改。现在BIOS存放在具有电可擦除编程特性的EPROM和Nor Flash芯片中。对于32位地址总线的系统来说,4GB的物理地址空间至少被划分为三个部分
:一部分是内存的空间,一部分用于对BIOS芯片存储单元进行寻址,还有一部分用于外部设备的板载存储空间的寻址。
x86复位后工作在实模式下,该模式下CPU的寻址空间为1MB。CS:IP的复位值为FFFF:0000,物理地址为FFFF0。
主板的设计者必须保证把这个物理地址映射到BIOS芯片上,而不是RAM。早期的IBM PC物理地址空间映射如下图所示:
其中高256K映射到BIOS ROM芯片中;中间的128K映射到视频卡的存储空间(老式的VGA显示模式直接往这段显存写数据,就可以显示。现在估计只有bios阶段使用这种显示方式,系统起来后会开启更高级的显卡显示模式。)剩下的640KB映射到RAM上。可以看出对于硬件系统的设计者来说,物理地址空间也是一种资源,而这里所说的映射就是以硬件方式对物理地址资源的分配。
640KB的RAM是BIOS设计者自由使用的区域,如何使用取决于BIOS软件的设计者。
前面说到CPU的CS:IP指向物理地址0xFFFF0,现在开始执行BIOS代码对系统进行必要的初始化,并在物理地址0开始的1KB内存中建立实模式下的中断向量表,随后的256byte被用来保存BIOS检测到的硬件信息
最后BIOS会根据配置把引导设备的第一个扇区加载到物理地址0x07C00,通常引导设备的第一个扇区是boot loader(比如GRUB)的代码,然后跳转到0x07C00继续执行。boot loader接着把内核加载到内存中。(这里内核分成两个部分:setup是实模式的代码,vmlinux是保护模式的代码,在编译时合成vmlinuz)。内核加载完毕后物理内存布局(图1):
~ ~
| Protected-mode kernel | 1M处,这里加载的是vmlinux
100000 +------------------------+
| I/O memory hole | 就是上图中的 ROM + VDR
0A0000 +------------------------+ 640K
| Reserved for BIOS | Reserved for BIOS EBDA(Extended BIOS Data Area)
~ ~
| Command line | (Can also be below the X+10000 mark)
X+10000 +------------------------+
| Stack/heap | 实模式setup所使用的堆栈
X+08000 +------------------------+
| Kernel setup | The kernel real-mode code.
| Kernel boot sector | The kernel legacy boot sector. 可以假设X 为0x10000(64K)
X +------------------------+
| BIOS中断服务程序(8K) | 起始地址为0x0E2CE(56K)
+------------------------+
| Boot loader | <- Boot sector entry point 0000:7C00
001000 +------------------------+ 4K
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+ 1.5K
| BIOS use only | BIOS中断向量表(1K) 与 硬件信息(256B)
000000 +------------------------+ setup前面的boot sector并不是用来引导系统,而是为了兼容老式的引导(直接把整个vmlinuxz写到软盘上,从软盘引导启动)以及`传递一些参数`(先别急琢磨内核参数怎么通过boot sector进行传递,后面会有讲解)。Boot loader在加载完内核后,会跳转到setup的入口点。
实模式setup阶段
setup用于体系结构相关的硬件初始化工作,在arch目录各个平台中都有类似功能的代码。在32位的x86平台中,setup的入口点是arch/x86/boot/header.S中的_start:
header.S
.code16
.section ".bstext", "ax"
.global bootsect_start
bootsect_start:
# Normalize the start address
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
......
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
# Kernel attributes; used by setup. This is part 1 of the
# header, from the old boot sector.
.section ".header", "a"
.globl hdr
hdr:
setup_sects: .byte SETUPSECTS #该扇区后面的实模式代码占用几个扇区
root_flags: .word ROOT_RDONLY
syssize: .long SYSSIZE
ram_size: .word RAMDISK
vid_mode: .word SVGA_MODE
root_dev: .word ROOT_DEV
boot_flag: .word 0xAA55
# offset 512, entry point
.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:
......
code32_start: # here loaders can put a different
# start address for 32-bit code.
#ifndef __BIG_KERNEL__
.long 0x1000 # 0x1000 = default for zImage
#else
.long 0x100000 # 0x100000 = default for big kernel
#endif
现在的内核(即setup和vmlinux)都需要由Boot Loader来加载,如果像传统的那样直接从BIOS加载setup前面的boot sector,则会进入msg_loop,不断打印出错信息(见上面代码5-35行)。数据结构hdr(43行)用来传递一些信息给之后运行的代码,其中setup_sects用来指明该扇区后面的实模式代码占用几个扇区。
在图1中,Boot Loader把setup加载到X处后,DS寄存器设置为X,再跳转到DS:200H把控制权交到_start。示意图如下:
_start这里是硬编码一条跳转指令,它跳转至start_of_setup:
.section ".inittext", "ax"
start_of_setup:
#ifdef SAFE_RESET_DISK_CONTROLLER
# Reset the disk controller.
movw $0x0000, %ax # Reset disk controller
movb $0x80, %dl # All disks
int $0x13
#endif
# Force %es = %ds
movw %ds, %ax
movw %ax, %es
cld
movw %ss, %dx
cmpw %ax, %dx # %ds == %ss?
movw %sp, %dx
je 2f # -> assume %sp is reasonably set
# Invalid %ss, make up a new stack
#_end在编译链接时是相对于setup起始地址X,而bootloader在跳转至setup前已经把DS设置为X
#所以不会有重定位的问题。
movw $_end, %dx
testb $CAN_USE_HEAP, loadflags
jz 1f
movw heap_end_ptr, %dx
1: addw $STACK_SIZE, %dx # 在_end上再加上栈大小,作为实模式下setup程序的栈(还记得图1里的栈吗)
jnc 2f
xorw %dx, %dx # Prevent wraparound
2: # Now %dx should point to the end of our stack space
andw $~3, %dx # dword align (might as well...)
jnz 3f
movw $0xfffc, %dx # Make sure we're not zero
3: movw %ax, %ss # 设置堆栈段基地址为X
movzwl %dx, %esp # Clear upper half of %esp 设置栈顶指针
sti # Now we should have a working stack
# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
pushw %ds
pushw $6f
lretw
6:
......
# Zero the bss
movw $__bss_start, %di
movw $_end+3, %cx
xorl %eax, %eax
subw %di, %cx
shrw $2, %cx
rep; stosl
# Jump to C code (should not return)
calll main
......
上面代码的主要内容就是设置实模式下setup程序的栈。10-35行建立堆栈,堆栈段寄存器SS为X,栈顶指针为_end+STACK_SIZE。示意图如下:
接下来跳转到main函数执行:
main.c
arch\x86\boot\main.c
void main(void)
{
/* First, copy the boot header into the "zeropage" */
copy_boot_params();
/* End of heap check */
if (boot_params.hdr.loadflags & CAN_USE_HEAP) {
heap_end = (char *)(boot_params.hdr.heap_end_ptr
+0x200-STACK_SIZE);
} else {
/* Boot protocol 2.00 only, no heap available */
puts("WARNING: Ancient bootloader, some functionality "
"may be limited!\n");
}
/* Make sure we have all the proper CPU support */
if (validate_cpu()) {
puts("Unable to boot - please use a kernel appropriate "
"for your CPU.\n");
die();
}
/* Tell the BIOS what CPU mode we intend to run in. */
set_bios_mode();
/* Detect memory layout */
detect_memory();
/* Set keyboard repeat rate (why?) */
keyboard_set_repeat();
/* Set the video mode */
set_video();
/* Query MCA information */
query_mca();
/* Voyager */
#ifdef CONFIG_X86_VOYAGER
query_voyager();
#endif
/* Query Intel SpeedStep (IST) information */
query_ist();
/* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
/* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
query_edd();
#endif
/* Do the last things and invoke protected mode */
go_to_protected_mode();
}
copy_boot_params把位于第一个扇区的参数复制到boot_params变量中(boot_params位于setup的数据段)。其后是一些设置和获取系统硬件参数有关的代码。main函数收集到的信息全部存放在setup数据段的boot_params中。示意图:
我们来看看该变量的定义:
struct boot_params
/* The so-called "zeropage" */
struct boot_params {
......
struct setup_header hdr; /* setup header */ /* 0x1f1 */
__u8 _pad7[0x290-0x1f1-sizeof(struct setup_header)];
__u32 edd_mbr_sig_buffer[EDD_MBR_SIG_MAX]; /* 0x290 */
struct e820entry e820_map[E820MAX]; /* 0x2d0 */
__u8 _pad8[48]; /* 0xcd0 */
struct edd_info eddbuf[EDDMAXNR]; /* 0xd00 */
__u8 _pad9[276]; /* 0xeec */
} __attribute__((packed));
比较重要的是setup_header
setup_header
struct setup_header {
//setup二进制中除boot loader外的代码所占用的扇区数
__u8 setup_sects;
__u16 root_flags;
__u32 syssize;
__u16 ram_size;
#define RAMDISK_IMAGE_START_MASK 0x07FF
#define RAMDISK_PROMPT_FLAG 0x8000
#define RAMDISK_LOAD_FLAG 0x4000
__u16 vid_mode;
__u16 root_dev;
__u16 boot_flag;
//到这里为止的参数是从boot sector中的hdr传递而来(由Boot Loader设置在里面的)
__u16 jump;
__u32 header;
__u16 version;
/* realmode_swtch用来设置回调函数
* setup在进入保护模式前回调该函数 main -> go_to_protected_mode -> realmode_switch_hook
*/
__u32 realmode_swtch;
__u16 start_sys;
__u16 kernel_version;
//boot loader的类别,不同的boot loader有唯一的编号
__u8 type_of_loader;
__u8 loadflags;
#define LOADED_HIGH (1<<0)
#define KEEP_SEGMENTS (1<<6)
#define CAN_USE_HEAP (1<<7)
__u16 setup_move_size;
/* 保护模式代码入口,由boot loader设置,至于怎么设置的,内核和boot loader遵从了一组协议 见Documentation\i386\boot.txt */
__u32 code32_start;
/* Boot Loader把 Ramdisk 镜像文件加载到内存
* 并设置该参数指向内存的起始地址
*/
__u32 ramdisk_image;
/* Ramdisk镜像文件大小 */
__u32 ramdisk_size;
__u32 bootsect_kludge;
__u16 heap_end_ptr;
__u16 _pad1;
/* Boot Loader存放内核参数的 32位线性地址 */
__u32 cmd_line_ptr;
__u32 initrd_addr_max;
__u32 kernel_alignment;
__u8 relocatable_kernel;
__u8 _pad2[3];
__u32 cmdline_size;
__u32 hardware_subarch;
__u64 hardware_subarch_data;
} __attribute__((packed));
接下来我们看看参数传递的详细过程
copy_boot_params
static void copy_boot_params(void)
{
struct old_cmdline {
u16 cl_magic;
u16 cl_offset;
};
const struct old_cmdline * const oldcmd =
(const struct old_cmdline *)OLD_CL_ADDRESS;
BUILD_BUG_ON(sizeof boot_params != 4096);
memcpy(&boot_params.hdr, &hdr, sizeof hdr);
if (!boot_params.hdr.cmd_line_ptr &&
oldcmd->cl_magic == OLD_CL_MAGIC) {
/* Old-style command line protocol. */
u16 cmdline_seg;
/* Figure out if the command line falls in the region
of memory that an old kernel would have copied up
to 0x90000... */
if (oldcmd->cl_offset < boot_params.hdr.setup_move_size)
cmdline_seg = ds();
else
cmdline_seg = 0x9000;
boot_params.hdr.cmd_line_ptr =
(cmdline_seg << 4) + oldcmd->cl_offset;
}
}
这里主要是把header.S中的参数拷贝到boot_params.hdr中。
Boot Loader可以向内核传递命令,它把参数放到适当的位置(见图一 Command line),然后本函数将其地址设置到boot_params.hdr.cmd_line_ptr。这里设置的是Command line的物理地址,因为后面进入保护模式后,低端线性地址直接映射到对应的物理地址。main函数最后调用go_to_protected_mode进入保护模式。
go_to_protected_mode
void go_to_protected_mode(void)
{
/* Hook before leaving real mode, also disables interrupts */
//还记得之前介绍的setup_header结构吗,这个函数会执行setup_header-> realmode_swtch回调函数
//Boot Loader可以利用realmode_swtch抓住在实模式下执行某些代码的最后机会
realmode_switch_hook();
/* Move the kernel/setup to their final resting places */
move_kernel_around();
/* Enable the A20 gate */
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}
/* Reset coprocessor (IGNNE#) */
reset_coprocessor();
/* Mask all interrupts in the PIC */
mask_all_interrupts();
/* Actual transition to protected mode... */
setup_idt();
setup_gdt();//建立临时的数据段和代码段供保护模式使用,其端基全为0
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));
}
主要是设置gdt和idt,gdt临时的数据段和代码段端基地址都为0。boot_params.hdr.code32_start为32位保护模式下的代码入口地址,由Boot Loader把bzImage的第二部分搬到合适的地方后,设置该参数。在这里在传递boot_params的地址时,加上了 (ds() « 4)。因为等下进入保护模式后,代码段和数据段基地址都会变为0,所以这里直接传递物理地址。
(由于本人觉得2.6.24内核的protected_mode_jump是有问题的,所以这里搬来了2.6.32的protected_mode_jump进行分析)
/*
* void protected_mode_jump(u32 entrypoint, u32 bootparams);
* entrypoint 和 bootparams 都是段基为0的线性地址
*/
GLOBAL(protected_mode_jump)
movl %edx, %esi # 将boot_params设置到esi作为在32位保护模式下执行函数的第二个参数
xorl %ebx, %ebx
movw %cs, %bx
shll $4, %ebx
addl %ebx, 2f #使标号2处的函数地址变为线性地址
jmp 1f # Short jump to serialize on 386/486 清除流水线队列
1:
movw $__BOOT_DS, %cx
movw $__BOOT_TSS, %di
movl %cr0, %edx
orb $X86_CR0_PE, %dl # Protected mode
movl %edx, %cr0 # 进入保护模式,此时还没开启页表,因此EIP就是物理地址
# Transition to 32-bit mode
.byte 0x66, 0xea # ljmpl opcode
2: .long in_pm32 # offset
.word __BOOT_CS # segment 这里更新CS
ENDPROC(protected_mode_jump)
.code32
.section ".text32","ax"
GLOBAL(in_pm32)
# Set up data segments for flat 32-bit mode
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
# The 32-bit code sets up its own stack, but this way we do have
# a valid stack if some debugging hack wants to use it.
addl %ebx, %esp
# Set up TR to make Intel VT happy
ltr %di
# Clear registers to allow for future extensions to the
# 32-bit boot protocol
xorl %ecx, %ecx
xorl %edx, %edx
xorl %ebx, %ebx
xorl %ebp, %ebp
xorl %edi, %edi
# Set up LDTR to make Intel VT happy
lldt %cx
jmpl *%eax # Jump to the 32-bit entrypoint 跳转目标从寄存器中读出
ENDPROC(in_pm32)
protected_mode_jump最后开启CR0寄存器的PE位,进入保护模式,更新各个段寄存器。最后 jmpl *%eax,eax保存的是调用protected_mode_jump时的第一个参数。从boot_params.hdr.code32_start处继续运行。示意图如下:
保护模式start_up32
现在我们进入vmlinuz的第二部分,这部分的代码都位于arch\x86\kernel目录下,入口是startup_32,内核代码中有两个startup_32,一个位于arch\x86\kernel\head_32.S,另一个位于arch\x86\boot\compressed\head_32.S。这里跳转的入口是哪一个呢?arch\x86\kernel\head_32.S是编译到内核vmlinux中,vmlinux被objcopy处理再经过压缩,最后被当成数据段和arch\x86\boot\compressed\head_32.S链接。所以先执行的是compressed\head_32.S。这段汇编文件代码会调用decompress_kernel,在它将kernel解压后跳转到kernel代码开始执行kernel代码,kernel的入口代码是在arch/x86/kernel/head_32.S
.section .text.head,"ax",@progbits
ENTRY(startup_32)
......
/*
* Set segments to known values.
* 加载新的GDT,重新初始化 段选择子
*/
1: lgdt boot_gdt_descr - __PAGE_OFFSET
movl $(__BOOT_DS),%eax
movl %eax,%ds
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
2:
/*
* Clear BSS first so that there are no surprises...
* BSS是未初始化变量区,把这个区初始化为0
*/
cld
xorl %eax,%eax
movl $__bss_start - __PAGE_OFFSET,%edi
movl $__bss_stop - __PAGE_OFFSET,%ecx
subl %edi,%ecx
shrl $2,%ecx
rep ; stosl
/*
* Copy bootup parameters out of the way.
* Note: %esi still has the pointer to the real-mode data.
* With the kexec as boot loader, parameter segment might be loaded beyond
* kernel image and might not even be addressable by early boot page tables.
* (kexec on panic case). Hence copy out the parameters before initializing
* page tables.
*/
movl $(boot_params - __PAGE_OFFSET),%edi #指定参数复制的目的地,这里的boot_params定义在arch\x86\kernel\setup_32.c,它被链接到vmlinux的数据段中
movl $(PARAM_SIZE/4),%ecx #大小
cld
rep
movsl # 还记得之前在 protected_mode_jump 中的 movl %edx, %esi(edx保存有setup中的boot_params的地址)这里就是把setup中的boot_params 复制到vmlinux中的boot_params
movl boot_params - __PAGE_OFFSET + NEW_CL_POINTER,%esi # 把__u32 cmd_line_ptr赋值给esi
andl %esi,%esi
jz 1f # No comand line
movl $(boot_command_line - __PAGE_OFFSET),%edi
movl $(COMMAND_LINE_SIZE/4),%ecx
rep
movsl #把boot_params中hdr.cmd_line_ptr指向的内容复制到vmliux数据区的 boot_command_line
# 也就是把boot loader设置的1M一下的命令行参数复制到 vmlinux的数据段boot_command_line
1:
......
boot_gdt_descr:
.word __BOOT_DS+7
.long boot_gdt - __PAGE_OFFSET
ENTRY(boot_gdt)
.fill GDT_ENTRY_BOOT_CS,8,0
.quad 0x00cf9a000000ffff /* kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* kernel 4GB data at 0x00000000 */
因为整个内核的第二部分链接的起始地址是0xC0100000,所以如果直接访问其中的符号,其地址必然在0xC0100000之上。现在还没有启用分页,所以访问这些符号就得减去__PAGE_OFFSET(0xC0000000)。
这里还把1M以下setup中的参数复制到vmlinux的数据区的boot_params(35-39行)。同样内核命令行参数也在这里(第41-48行)被复制到vmlinux的数据段的boot_command_line。
接下来的工作就是初始化页表,为开启分页做准备。
page_pde_offset = (__PAGE_OFFSET >> 20); #3K,用来在页目录swapper_pg_dir中定位高1G线性地址
default_entry:
#pg0是在链接脚本vmlinux.lds中定义的,它位于内核数据段结束的地方
movl $(pg0 - __PAGE_OFFSET), %edi
#swapper_pg_dir 内核页目录的起始地址,占据4K大小。位于vmlinux的数据段中
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */
10:
# 这里%ecx的值为 pg0+n*4K+0x007 n为当前的循环次数,从0开始
leal 0x007(%edi),%ecx /* Create PDE entry */
# 下面两行代码设置swapper_pg_dir页目录,3K偏移的部分也复制一份
# 也就是下图的第768项,它和第一项是一样的。为什么要往3K部分复制一份?
# 因为后面开启分页后,就可以直接用符号的线性地址
movl %ecx,(%edx) /* Store identity PDE entry */
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
addl $4,%edx # edx也增加了,edx用来对swapper_pg_dir页目录项进行寻址
movl $1024, %ecx #下面的循环执行1024次。上面页目录设置完了,接下来开始设置pg0页表
11:
stosl # 将EAX中的值保存到ES:EDI指向的地址中。也就是 0x007 --> pg0 且 edi=edi+4
addl $0x1000,%eax # 准备下一个物理页的物理地址(这里为什么+0x1000,因为每个物理页大小为4K)
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp # edi代表已经映射了的物理页表项的最高地址
cmpl %ebp,%eax
jb 10b # 直到映射完整个内核所占物理地址空间
#记录下已经生成的Page Table的最高物理地址(pg0+n*4K)
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
/* Do an early initialization of the fixmap area */
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
movl $(swapper_pg_pmd - __PAGE_OFFSET), %eax
addl $0x67, %eax /* 0x67 == _PAGE_TABLE */
movl %eax, 4092(%edx)#把swapper_pg_pmd 设置到页目录swapper_pg_dir的最后一项
#关于其属性0x67。swapper_pg_pmd页表可以被读、写或执行,运行在任何特权级上的程序都可以访问该页表
xorl %ebx,%ebx /* This is the boot CPU (BSP) */
jmp 3f
3:
/*
* Enable paging
*/
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */
# 已经开启分页
ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip 让EIP使用大于3G的线性地址*/
1:
/* Set up the stack pointer */
lss stack_start,%esp
......
ENTRY(swapper_pg_dir)
.fill 1024,4,0
ENTRY(swapper_pg_pmd)
.fill 1024,4,0
ENTRY(stack_start)
.long init_thread_union+THREAD_SIZE
.long __BOOT_DS
已经建立的虚拟地址空间映射:
从虚拟地址0建立起来的映射在后面会很快被撤销,在内核中虚拟地址空间从0开始的一段区域是不映射。专门用于NULL指针检测的。线性地址空间的最后一G,专门用来映射整个内核。这1G空间的开头部分在这里已经映射到物理地址的开头部分,将来会把更多的物理内存映射到这个虚拟地址空间上来。
进入start_kernel后,内核要建立一个启动时的内存管理机制bootmem_allocator,BOOTBITMAP_SIZE是bootmem_allocator需要使用的位图大小。
页表建立完成后,接下来开始设置堆栈(58行),堆栈段选择子ss为__BOOT_DS,和数据段在同一个段,栈顶指针为init_thread_union +THREAD_SIZE,THREAD_SIZE默认是8K。init_thread_union 在 arch\x86\kernel\init_task.c定义:
堆栈本身是被设置在vmlinux二进制的.data.init_task段。以后我们会看到内核会利用这个堆栈“手工”建立一个进程,最终在这个“手工”建立的进程环境下调用kernel_thread()创建init进程。
最终虚拟地址和物理地址的映射关系如下图所示,在设置好页目录和页表后,把页表结束的物理地址保存到init_pg_tables_end变量中,以后内核会用到这个变量,它记录了内核目前使用的最大物理内存地址。最后通过置位cr0的PG位进入分页模式。
开启分页设置好内核栈后,接下来开始中断初始化(call setup_idt)。首先,内核在arch\x86\kernel\traps_32.c文件中定义了一个中断描述符表,一共256项,每一项8个字节:
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };
idt_table默认是空的,现在call setup_idt去设置idt_table,只不过在启动过程之初,所有的中断处理程序都被临时设置为ignore_int。
setup_idt:
lea ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt_table,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi) #设置描述符项
movl %edx,4(%edi)
addl $8,%edi #指向下一个描述符项
dec %ecx
jne rp_sidt #不断循环设置
......
从setup_idt返回后,接下来就设置idtr寄存器,并最终jump 到start_kernel函数执行:
.....
lgdt early_gdt_descr #又换了GDT,
lidt idt_descr
ljmp $(__KERNEL_CS),$1f #更新CS
1: movl $(__KERNEL_DS),%eax # reload all the segment registers
movl %eax,%ss # after changing gdt.
movl %eax,%fs # gets reset once there's real percpu
movl $(__USER_DS),%eax # DS/ES contains default USER segment
movl %eax,%ds
movl %eax,%es
xorl %eax,%eax # Clear GS and LDT
movl %eax,%gs
lldt %ax
cld # gcc2 wants the direction flag cleared at all times
pushl $0 # fake return address for unwinder
#ifdef CONFIG_SMP
movb ready, %cl
movb $1, ready
cmpb $0,%cl # the first CPU calls start_kernel
je 1f #对于BP来讲,不会走下面的分支的。因为第一次执行,ready符号为0
movl $(__KERNEL_PERCPU), %eax
movl %eax,%fs # set this cpu's percpu
jmp initialize_secondary # all other CPUs call initialize_secondary
1:
#endif /* CONFIG_SMP */
jmp start_kernel
idt_descr:
.word IDT_ENTRIES*8-1 # idt contains 256 entries
.long idt_table
ENTRY(early_gdt_descr)
.word GDT_ENTRIES*8-1
.long per_cpu__gdt_page /* Overwritten for secondary CPUs */
现在终于进入start_kernel。
内核启动start_kernel
start_kernel是内核初始化的主要函数,在这个函数中主要对内核的各个模块进行初始化,包括内存,中断等。对某个具体模块初始化的介绍放到相关的其他文章中。
内核启动时的参数传递
看绿色箭头: