tobyjiang Linux Dev Engineer

Linux内核的引导启动


Linux内核启动流程分为如下几个阶段:

  1. BIOS启动阶段
  2. 实模式setup阶段
  3. 保护模式 startup_32
  4. 内核启动start_kernel()
  5. 内核启动时的参数传递

下面我们一一进行分析。阅读整篇文章的同时,可以参考内核源代码文档 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物理地址空间映射如下图所示:

1552962185146

其中高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。示意图如下:

1553777531168

_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。示意图如下:

1553778353545

接下来跳转到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中。示意图:

1553778713515

我们来看看该变量的定义:

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处继续运行。示意图如下:

1553780647230

保护模式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, %esiedx保存有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_paramshdr.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

已经建立的虚拟地址空间映射:

1553782021922

从虚拟地址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定义:

union thread_union init_thread_union
	__attribute__((__section__(".data.init_task"))) =
		{ INIT_THREAD_INFO(init_task) };

堆栈本身是被设置在vmlinux二进制的.data.init_task段。以后我们会看到内核会利用这个堆栈“手工”建立一个进程,最终在这个“手工”建立的进程环境下调用kernel_thread()创建init进程。

最终虚拟地址和物理地址的映射关系如下图所示,在设置好页目录和页表后,把页表结束的物理地址保存到init_pg_tables_end变量中,以后内核会用到这个变量,它记录了内核目前使用的最大物理内存地址。最后通过置位cr0的PG位进入分页模式。

1553154284135

开启分页设置好内核栈后,接下来开始中断初始化(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是内核初始化的主要函数,在这个函数中主要对内核的各个模块进行初始化,包括内存,中断等。对某个具体模块初始化的介绍放到相关的其他文章中

内核启动时的参数传递

看绿色箭头:

1553784194521


Similar Posts

上一篇 SMP系统的引导

Comments