Chinaunix首页 | 论坛 | 博客
  • 博客访问: 365843
  • 博文数量: 181
  • 博客积分: 215
  • 博客等级: 民兵
  • 技术积分: 313
  • 用 户 组: 普通用户
  • 注册时间: 2012-05-17 19:39
个人简介

王的男人

文章分类

全部博文(181)

文章存档

2016年(2)

2015年(35)

2014年(17)

2013年(84)

2012年(49)

我的朋友

分类:

2012-07-18 23:50:21

原文地址:linux内核引导代码分析 作者:gxy_0202

编译Linux内核映像
要想了解Linux内核的引导过程就必须知道Linux内核的编译过程。这一部分说明编译Linux内核的步骤和编译过 程每一步产生的输出。编译过程依赖体系结构所以我要强调我所说的是指编译基于x86体系结构的Linux内核。首先用户使用make config或make menuconfig命令配置内核,然后输入make、make zImage或make bzImage后编译生成可引导的内核映像存放在arch/i386/boot/zImage或者arch/i386/boot/bzImage处,下面 是内核映像的生成过程。

1)C和汇编源文件被编译成ELF可重定位object文件(.o),相当于windows的.obj文件,其中一部分按照逻辑分组用ar命令打包成压缩文件(即静态链接库文件.a)。

(ar命令用来创建、修改库,也可以从库中提出单个模块,.a文件就是一系列.o文件的压缩包。)

2)调用ld命令将以上的.o和.a文件链接成一个静态的non-stripped的ELF格式的在80386 32位平台上运行的可执行文件vmlinux。

(ld命令把一定量的目标文件和档案文件链接起来, 重定位他们的数据,non-stripped表示没有去除符号表,可以使用函数、变量名访问,用于debug过程;而strip之后只能通过地址访问。vmlinux就是未压缩的Linux内核。)

3)调用nm vmlinux 指令剔除不相关和不感兴趣的符号并创建内核符号表。

(例 如nm /boot/vmlinux-2.4.7-10 > System.map。内核符号表是内核变量地址和变量名的对应关系,每次编译时产生新的,内核通过地址识别符号,而用户需要使用符号名编码和链接,例如 内核日志记录后台程序,通过System.map便于对内核的调试。)

4)进入arch/i386/boot目录。

5)Bootsect.S文件按照目标是bzImage或zImage在定义或不定义 –D_BIG_KERBEL_ 宏下进行预处理,结果分别存为bbootsect.s或bootsect.s。

6)bbootsect.s文件被编译并转换成“raw binary”格式的bbootsect文件(bootsect.s 被转换成“raw”格式文件bootsect)。

7)setup.S(setup.S 包含了video.S文件)被预处理成bzImage需要的bsetup.s或者zImage需要的setup.s文件。这个过程和bootsector 一样,bzImage镜像需要定义-D__BIG_KERNEL__宏,结果被转换成“raw binary”格式的bsetup,zImage镜像则被转换成“raw binary”格式的setup。

8) 进入arch/i386/boot/compressed目录,移出/usr/src/linux/vmlinux文件中ELF标识节.note和. comment ,并将其转换成raw binary格式存放到临时文件$tmppiggy。

9) 将$tmppiggy用gzip命令压缩成$tmppiggy.gz

10) 用ld -r命令将$tmppiggy.gz链接成可重定向的ELF格式文件piggy.o

11) 把压缩程序head.S 和misc.c文件编译成head.o和misc.o。

如 果kernel压缩过,则要进行解压,在压缩过的kernel头部有解压程序。压缩过的kernel入口第一个文件源码位置在arch/i386 /boot/compressed/head.S。它将调用函数decompress_kernel(),这个函数在文件arch/i386/boot /compressed/misc.c中,decompress_kernel()又调用 proc_decomp_setup(),arch_decomp_setup()进行设置,然后使用在打印出信息“Uncompressing Linux...”后,调用gunzip()。将内核放于指定的位置。

启动首先运行的文件有:

arch/i386/boot/compressed/head.S

arch/i386/boot/compressed/head-xscale.S

arch/i386/boot/compressed/misc.c

12) 将head.o、misc.o和piggy.o链接成bvmlinux(或者vmlinux),注意vmlinux的标号-Ttext 0x1000和bvmlinux的标号-Ttext 0x100000的不同,这是由于bzImage压缩装载器是从高位装载的。

13)将bvmlinux转换成“raw binary”文件bvmlinux.out,移出ELF的.note 和.comment标识节。

14) 回到arch/i386/boot目录并调用tools/build将bbootsect、bsetup和压缩后的bvmlinux链接成 bzImage。这个过程将向bootsector末尾添加重要的变量(4个字节)例如setup_sects和root_dev。

bootsector的大小总是512字节,setup的大小必须大于4个扇区且受限于12K,规则如下:

0x4000 bytes >= 512 + setup_sects*512 + 运行bootsector/setup所需堆栈空间。

在后面将说明是哪个部分造成了这种限制。

bzImage文件大小的上限采用LILO启动时为2.5M,采用冷启动如软盘或者光盘等则为0xFFFF0(1048560)字节。

注意,tools/build工具检验了bootsector的大小、内核映像的大小和setup的低范围地址,并没有检测setup的高范围地址。因此,在setup.S文件末尾的“.space”节增加一个大的地址数值就会很容易创建一个无法使用的内核。



2. 引导:概述

启动过程是和体系结构相关的,这里仅关注PC/IA32体系。由于旧有设计以及向前兼容,PC机采用了以前流行的风格启动操作系统。这个过程可以被分为一下六个逻辑步骤:

1) BOIS选择启动设备。

2)从启动设备装载bootsector。

3)Bootsector装载setup、解压缩程序和内核映像。

4)在保护模式下解压内核。

5)汇编代码执行低级初始化(主要是对硬件如CPU和内存的初始化)。

6)执行上层C语言的初始化。

当 PC的电源打开后,80x86结构的CPU将自动进入实模式,对于从硬盘启动的设备将从地址0xFFFF0开始自动执行程序代码,这个地址通常是ROM- BIOS中的地址。PC机的BIOS将执行某些系统的检测,在物理地址0处开始初始化中断向量。此后,它将可启动设备的第一个扇区读入内存地址 0x7C00处,并跳转到这个地方。Linux的最最前面部分是用汇编语言编写的(boot/bootsect.S),它将由BIOS读入到内存 0x7C00处,当它被执行时就会把自己移到绝对地址0x90000处,并将启动设备(boot/setup.S)的下2kB字节的代码读入内存 0x90200处,而内核的其它部分则被读入到地址0x10000处。在系统加载期间将显示信息"Loading..."。然后控制权将传递给 boot/Setup.S中的代码,这是另一个实模式汇编语言程序。head.s会把IDT(中断向量表)、GDT(全局段描述符表)、LDT(局部段描 述符表)的首地址装入到相应的寄存器里,初始化处理器和协处理器,设置好分页,最后调用init/main.c中的main()程序。



3.引导:BOIS POST

1)电源启动时钟发生器并在总线上产生一个#POWERGOOD的中断。

2)产生CPU的RESET中断(此时CPU处于8086工作模式)。

3) %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (ROM BIOS POST code).

4)在中断无效状态下执行所有POST检查。

5)在地址0初始化中断向量表IVT。

6) 0x19中断以启动设备号为参数调用BIOS启动装载程序。这个程序从启动设备(硬盘)的0扇面1扇区读取数据到内存物理地址0x7C00开始装载。

4. 引导:bootsector和setup

用来引导内核的bootsector可以是以下几种:

Linux bootsector(arch/i386/boot/bootsect.S)

LILO,GRUB(双系统)

Bootloader(嵌入式系统,例如U-boot)

以下详细解释linux bootsector。下面一些代码,负责初始化用作段变量的宏定义。

29 SETUPSECS = 4 /* default nr of setup-sectors */

30 BOOTSEG = 0x07C0 /* original address of boot.sector */

31 INITSEG = DEF_INITSEG /* we move boot here . out of the way */

32 SETUPSEG = DEF_SETUPSEG /* setup starts here */

33 SYSSEG = DEF_SYSSEG /* system loaded at 0x10000 (65536) */

34 SYSSIZE = DEF_SYSSIZE /* system size: # of 16-byte clicks */

在文件include/asm/boot.h中定义了DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG和DEF_SYSSIZE的值。

/* Don't touch these, unless you really know what you're doing. */

#define DEF_INITSEG 0x9000

#define DEF_SYSSEG 0x1000

#define DEF_SETUPSEG 0x9020

#define DEF_SYSSIZE 0x7F00

以下来看看bootsect.S的源代码:

54 movw $BOOTSEG, %ax

55 movw %ax, %ds

56 movw $INITSEG, %ax

57 movw %ax, %es

58 movw $256, %cx

59 subw %si, %si

60 subw %di, %di

61 cld

62 rep

63 movsw

64 ljmp $INITSEG, $go

65 # bde --changed 0xff00 to 0x4000 to use debugger at 0x6400 up (bde). We

66 # wouldn't have to worry about this if we checked the top of memory. Also

67 # my BIOS can be configured to put the wini drive tables in high memory

68 # instead of in the vector table. The old stack might have clobbered the

69 # drive table.

70 go: movw $0x4000-12, %di # 0x4000 is an arbitrary value >=

71 # length of bootsect + length of

72 # setup + room for stack;

73 # 12 is disk parm size.

74 movw %ax, %ds # ax and es already contain INITSEG

75 movw %ax, %ss

76 movw %di, %sp # put stack at INITSEG:0x4000-12.

代码54行~63行将bootsector从地址0x7C00移动到0x90000,由以下过程完成:

1) set %ds:%si to $BOOTSEG:0 (0x7C0:0 = 0x7C00)

2) set %es:%di to $INITSEG:0 (0x9000:0 = 0x90000)

3) set the number of 16bit words in %cx (256 words = 512 bytes = 1 sector)

4) clear DF (direction) flag in EFLAGS to auto-increment addresses (cld)

5) go ahead and copy 512 bytes (rep movsw)

The reason this code does not use rep movsd is intentional (hint-.code16).

代 码64行跳转到标号go:一个最新创建的bootsector的拷贝,也就是在0x9000段。这和接下来的三段指令(64~76行)在$ INITSEG:0x4000-0xC 段初始化一个堆栈,也就是指令%ss = $INITSEG (0x9000) 和 %sp = 0x3FF4 (0x4000-0xC)。这就是我们前面提到的setup大小限制的来历。
77~103行代码建立了第一个磁盘的参数表,以便允许多扇区读操作。

77 # Many BIOS's default disk parameter tables will not recognise

78 # multi-sector reads beyond the maximum sector number specified

79 # in the default diskette parameter tables . this may mean 7

80 # sectors in some cases.

81 #

82 # Since single sector reads are slow and out of the question,

83 # we must take care of this by creating new parameter tables

84 # (for the first disk) in RAM. We will set the maximum sector

85 # count to 36 . the most we will encounter on an ED 2.88.

86 #

87 # High doesn't hurt. Low does.

88 #

89 # Segments are as follows: ds = es = ss = cs . INITSEG, fs = 0,

90 # and gs is unused.

91 movw %cx, %fs # set fs to 0

92 movw $0x78, %bx # fs:bx is parameter table address

93 pushw %ds

94 ldsw %fs:(%bx), %si # ds:si is source

95 movb $6, %cl # copy 12 bytes

96 pushw %di # di = 0x4000-12.

97 rep # don't need cld -> done on line 66

98 movsw

99 popw %di

100 popw %ds

101 movb $36, 0x4(%di) # patch sector count

102 movw %di, %fs:(%bx)

103 movw %es, %fs:2(%bx)

通过0x13BOIS服务0号函数重置软盘管理器,并且在bootsector完成后立即载入setup部分。也就是说在物理地址0x90200 ($INITSEG:0x200)处再次调用0x13BOIS服务2号函数。这个过程发生在107~124行。

107 load_setup:

108 xorb %ah, %ah # reset FDC

109 xorb %dl, %dl

110 int $0x13

111 xorw %dx, %dx # drive 0, head 0

112 movb $0x02, %cl # sector 2, track 0

113 movw $0x0200, %bx # address = 512, in INITSEG

114 movb $0x02, %ah # service 2, "read sector(s)"

115 movb setup_sects, %al # (assume all on head 0, track 0)

116 int $0x13 # read it

117 jnc ok_load_setup # ok . continue

118 pushw %ax # dump error code

119 call print_nl

120 movw %sp, %bp

121 call print_hex

122 popw %ax

123 jmp load_setup

124 ok_load_setup:

如果由于某些原因出错,例如无法使用的软盘或者在运行过程中有人弹出了磁盘等,装载过程将输出错误代码并且无限循环尝试本过程。除非重试成功(这通常不会发生),如果出现其他错误后果将更严重,唯一退出这个循环的办法就是重新启动机器。

如 果成功装载配置代码部分,流程将跳转到ok_load_setup标签。紧接着,启动程序就在物理地址0x10000装载压缩后的内核。这样做是为了保护 低位(0~64K)内存的固件数据区。在内核装载后,启动程序跳转到地址 $SETUPSEG:0。一旦这些固件数据不再需要的时候,它们会被从0x10000移动到0x1000地址的完整内核镜像覆盖。这个过程由 setup.S完成,它主要设置保护模式下的状态,并跳转到压缩内核的起始物理地址0x1000,也就是arch/386/boot /compressed/{head.S,misc.c}文件。它设置堆栈,调用decompress_kernel()解压缩内核到0x100000并 跳转到该地址。

让我们分析一下bootsector代码里允许装载大内核(即bzImage)的组装部分。首先setup部分像往常 一样装载到地址 0x90200,但是采用调用BIOS服务将数据从低位内存移动到高位内存的辅助程序,内核一次可装载64K。这个辅助程序在bootsect.S中的 bootsect_kludge曾提到,并在setup.S中定义为bootsect_helper。Setup.S中的bootsect_kludge 标签段包含了setup段的代码以及其中bootsect_helper代码的偏移量,这样bootsector可以调用lcall指令跳转到 bootsect_helper。bootsect_helper包含在setup.S文件里的原因很简单,因为bootsect.S没有剩余的空间了。 这个程序调用0x15号BIOS服务以便移动到高位内存并复位%es,使其总是指向0x10000。这保证了bootsect.S里的代码在从磁盘拷贝数 据时不会溢出。

5.高级初始化

对于“高级初始化”我认为这不是直接和引导过程相关,即使它的部分实现代码也是用汇编语言编写的。也就是arch/i386/kernel/head.S文件,它是未压缩内核的最初部分。整个过程如以下部分:

1)初始化段寄存器的值(%ds = %es = %fs = %gs = __KERNEL_DS = 0x18)。

2)初始化内存页表。

3)设置%cr0的PG位,使内存分页机制有效。

4)将BSS清零(在SMP机上,仅第一个CPU会执行此操作)。

5)拷贝内核引导指令的前2k。

6)利用EFLAGS检测CPU类型,如果可能,还有cpuid,以便探测386或者更高型号。

7)第一个CPU调用start_kernel函数,如果ready等于1,其他CPU则调用arch/i386/kernel/smpboot.c文件的: initialize_secondary()函数,这个函数重新装载esp/eip且不再返回。

阅读(394) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~