编译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且不再返回。
阅读(494) | 评论(0) | 转发(0) |