linux内核引导代码分析ZZ

 

 

 

Linux作为开源软件的代表深受用户的支持,尤其是其在嵌入式系统领域更是具有广阔的发展前景,本文深入分析了Linux内核直接从硬盘引导

过程和部分代码。由于我一直在做基于x86体系结构的研究,读了两本关于Linux内核的书籍(<<Linux内核完全注释>>赵炯、<<Linux kernel 2.4 internals>>Tigran Aivazian)。所以这篇文章分析的代码是基于x86体系结构、Linux2.4版本内核的。但这个流程跟嵌入式系统中ARM的bootloader要完成的功能和原理是一致的。下面的工作是详细分析bootsect.S这个汇编程序代码完成的工作,对整个启动过程有进一步的了解。 


编译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且不再返回。 

这篇文章分析了Linux2.4内核基于x86体系结构的系统引导过程,和基于ARM的嵌入式平台原理是一样的。例如在ARM上移植U-boot时,也是要有三个文件load.bin、 boot.bin、u-boot.bin。它们共同点都是先从上电起始位置(硬件设置)跳转到ROM的一级bootloader处,经过处理,跳转到二级 bootloader处。完成引导,就可以启动内核,挂载文件系统了。我下一步的目标是进一步学习基于ARM平台嵌入式Linux的bootloader和Linux的文件系统在嵌入式系统上的移植  

 

转载于:https://www.cnblogs.com/sunt/archive/2010/12/29/1921172.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值