Linux内核笔记-启动协议

Linux 是非常出色的操作系统,通过阅读内核源码,能够清楚的理解现代操作系统的构架和任意细节,但 Linux 内核代码非常庞大,而且有很多与硬件相关的细节,阅读代码有些吃力,不过只要了解内核代码的构架,可对自己迷惑的部分单独分析。

我在这里对我分析的部分做一个记录,一来防止以后遗忘,二来把分析过程写出来能更加清楚的理解内核。分析的内核版本为 2.6.34,可从 这里 下载,或者使用 lxr 在线阅读代码。

Linux源码 arch 目录下是构架相关的代码,其中有x86,arm等不同平台的实现,这里仅看x86平台,boot目录是系统启动部分代码,我将会逐一分析启动代码。

2.6.34 版本较之以前有了很大的变动,内核启动过程需要大量的汇编操作,这个版本尽量去除汇编,仅仅在必须的地方提供汇编提供接口,供c代码调用。

先来看引导协议部分代码, arch/x86/boot/header.S

在之前BIOS中断部分讲过引导程序,位于启动介质0柱面0磁道1扇区的512字节,以0x55AA结尾,header.S中也同样含有这段代码,但是仅仅输出几行文字,并没有做实质性操作,Linux内核把引导的工作交付给专业的引导程序如grub、lilo,BIOS启动时首先加载grub程序,grub作为引导程序根据配置再去加载相应的内核。如此一来便需要grub和内核之间遵循一定的协议,协议文档位于 linux-2.6.34\Documentation\x86\boot.txt,可以作为参考。

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

bs_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

	.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

	# 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 0			/* Filled in by build.c */
root_flags:	.word ROOT_RDONLY
syssize:	.long 0			/* Filled in by build.c */
ram_size:	.word 0			/* Obsolete */
vid_mode:	.word SVGA_MODE
root_dev:	.word 0			/* Filled in by build.c */
boot_flag:	.word 0xAA55

这部分是完整的引导区代码,这段代码有三个段,.bstext .bsdata .header,由编译脚本setup.ld可以看到,.header是编译到497地址,除去一些变量,到boot_flag: .word 0xAA55刚好位于512字节,是一个引导扇区的长度。.bsdata 段仅定义了一个字符串,指明boot方式不再支持。

来看下代码部分 .bstext 段,首先由start2初始化一些变量,msg_loop调用int $0×10输出文字,bs_die部分首先调用int $0×16输入一个字符,然后调用int $0×19重启系统。

可以看到这里的引导程序并没有做什么操作,也不会被执行到,真正的代码再之后才开始,之前说过内核的引导是靠grub等引导程序,他们之间有一定的协议,协议有一个协议头,再看header.S之后的代码都是一些变量的定义,所以得先看一下协议头到底是什么,在boot.txt文档中有以下内容:

The header looks like:

Offset	Proto	Name		Meaning
/Size

01F1/1	ALL(1	setup_sects	The size of the setup in sectors
01F2/2	ALL	root_flags	If set, the root is mounted readonly
01F4/4	2.04+(2	syssize		The size of the 32-bit code in 16-byte paras
01F8/2	ALL	ram_size	DO NOT USE - for bootsect.S use only
01FA/2	ALL	vid_mode	Video mode control
01FC/2	ALL	root_dev	Default root device number
01FE/2	ALL	boot_flag	0xAA55 magic number
0200/2	2.00+	jump		Jump instruction
0202/4	2.00+	header		Magic signature "HdrS"
0206/2	2.00+	version		Boot protocol version supported
...
0258/8	2.10+	pref_address	Preferred loading address
0260/4	2.10+	init_size	Linear memory required during initialization

说的正是协议头的定义,这里的01F1/1前面表示地址,后面表示大小,01F1十进制为497,刚好与.header段的起始地址相同,看来,.header段名副其实,正是启动协议的头部信息,一直到236行的init_size定义为止。头部内容的详细信息在boot.txt文件中已有详细解释,不过现在还不需要知道,头部大部分信息由grub填充,供操作系统使用。后面会看到内核获取这些信息。

好了,header.S后面的.entrytext段就是真正的内核入口了。

	movw	$0x0000, %ax		# Reset disk controller
	movb	$0x80, %dl		# All disks
	int	$0x13

这里调用BIOS的13号中断重置硬盘。

	movw	$__bss_start, %di
	movw	$_end+3, %cx
	xorl	%eax, %eax
	subw	%di, %cx
	shrw	$2, %cx
	rep; stosl

__bss_start是bss段的起始地址,_end为结束位置,这两个符号定义于setup.ld脚本,这段代码既是对此段清零。

接下来调用main函数 calll main,这里的main是c函数,定义于main.c。

之后有符号setup_bad,调用puts输出”No setup signature found…\n”,最后定义了一个函数die,执行休息指令hlt的死循环。

最后再来看main函数中的开始部分代码 linux-2.6.34\arch\x86\boot\main.c

void main(void)
{
	/* First, copy the boot header into the "zeropage" */
	copy_boot_params();

	/* End of heap check */
	init_heap();
	...

可以看到main首先调用copy_boot_params函数,在29行有其定义,其中关键的一句如下:

memcpy(&boot_params.hdr, &hdr, sizeof hdr);

hdr 是引导协议头部0x1f1地址处,即刚才分析的.header段开始的标号,boot_params为结构体boot_params的实例,存储启动参数相关信息,结构定义于arch/x86/include/asm/bootparam.h:85,成员hdr定义为struct setup_header hdr;setup_header结构定义于相同文件的24行:

struct setup_header {
	__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;
	__u16	jump;
	__u32	header;
	__u16	version;
	__u32	realmode_swtch;
	__u16	start_sys;
	__u16	kernel_version;
	...

可以看到其中的成员正是引导协议头部,拷贝代码正是把grub填充的启动信息复制到结构实例boot_params.hdr中,以便在c程序中使用。

最后总结一下grub的引导过程,grub被BIOS载入内存并获得控制权后,读取配置信息/boot/grub.conf,载入实模式代码到0×090000,并根据内核镜像的前512字节中的.header信息,将大内核载入到内存,并交控制权于内核执行。载入内存地址的计算公式为:

is_bzImage = (protocol >= 0x0200) && (loadflags & 0x01);
load_address = is_bzImage ? 0x100000 : 0x10000;

这里通过分析header.S,对引导协议做了一些说明,更详尽的说明在文档 Documentation\x86\boot.txt,可随时对其查阅。

Built with Hugo
主题 StackJimmy 设计