-- linux爱好者,业余时间热衷于分析linux内核源码 -- 目前主要研究云计算和虚拟化相关的技术,主要包括libvirt/qemu,openstack,opennebula架构和源码分析。 -- 第五届云计算大会演讲嘉宾 微博:@Marshal-Liu
分类: LINUX
2010-07-31 16:10:03
一。bootloader介绍
bootloader是硬件在加电开机后,除BIOS固化程序外最先运行的软件,负责载入真正的操作系统,可以理解为一个超小型的os。目前在Linux平台中主要有lilo、grub等,在Windows平台上主要有ntldr、bootmgr、grldr等。这里以grub-0.97为基础描述bootloader的启动过程。
一般grub主要分为stage1和stage2两个阶段。stage1作为启动设备的MBR存在于第一扇区,大小只有512字节。stage1加载位于第二扇区的start程序,然后start以磁盘扇区形式而非文件系统形式载入stage2。stage2中包含了可以进行用户交互的处理流程,实际上就是一个小型的os。通过stage2可以选择决定载入的操作系统版本和相关参数,另外stage2还提供一些特殊功能,如加密、网络以及光盘启动等。
如果grub支持stage1_5,则stage1加载的start不是直接去加载stage2,而是先加载stage1_5,然后通过stage1_5支持的文件系统驱动,通过文件系统加载stage2。
要特别指出的是,start.S即是stage1_5的开头512字节即start程序的源码,同时也是 stage2的开头512字节的源码,只是里面一些具体的过程和参数因为条件编译不同而不同,比如在编译stage1_5的start时使用了 -DSTAGE1_5,而编译stage2时则没有。
stage1位于MBR扇区,即0面0磁道的第1扇区,大小为512字节(388字节代码+58字节BIOS参数块BPB信息+64字节分区表+2字节标志55AA)。start程序位于0面0道第2扇区。系统如果支持stage1_5,stage1_5一般从0面 0磁道的第3扇区开始,这时候stage2就可以以文件方式载入,否则stage2一般就从0面0磁道的第3扇区开始。这些都是grub在安装到系统的时候就准备就绪的。
二。grub的启动过程
1。系统加电,BIOS自检硬件状态,如CPU、内存、硬盘等信息。
2。BIOS执行INT 0x19,读取启动设备的MBR,即起始扇区的512字节,实际上就是grub的stage1,将其加载至内存地址0x7c00处并跳转执行。注意当最初的系统安装时,在安装grub的stage1到启动设备起始扇区的时候,grub的安装程序会在stage1中嵌入stage1_5或者stage2的磁盘位置信息,有了这个准备,stage1才可以在没有文件系统支持的情况下载入stage2。
3。stage1开始执行,加载位于第二扇区的start程序到0x2000(若支持stage1_5)或0x8000(不支持stage1_5),并跳转执行。
4。执行_start(在文件start.S中),若支持stage1_5,则加载stage1_5到0x2200,否则加载stage2到0x8200,并跳转执行。
5。执行EXT_C(main) (在文件asm.S中) ,通过EXT_C(init_bios_info)进入cmain。
6。执行cmain(在文件stage1_5.c或stage2.c中都有)。若是支持stage1_5,则先进入stage1_5中的cmain,通过文件系统载入stage2,然后执行chain_stage2,跳转到stage2中的EXT_C(main) ,再进入EXT_C(init_bios_info),然后是stage2中的cmain,否则直接进入stage2中的cmain。
7。调用run_menu,可进行用户交互选择启动内核。
8。执行run_script(在文件cmdline.c中),依次运行menu.lst(grub.conf) 中的builtin->func,如root_func、kernel_func及initrd_func等(详见文件builtins.c中),最后运行boot_func启动内核 。
三。硬盘工作模式和相关BIOS调用介绍
1。硬盘工作模式
现在的硬盘一般都支持逻辑块寻址(LBA)和柱面磁头扇区寻址(CHS)模式,CHS模式是指柱面/磁头/扇区 (Cylinder/Head/Sector) 组成的3D寻址方式。在磁盘的CHS寻址方式中,数据传输的地址是写到4个8位寄存器里的,分别是:柱面低位寄存器、柱面高位寄存器、扇区寄存器和设备/ 磁头寄存器。
柱面地址是16位,即柱面低位寄存器(8位)加上柱面高位寄存器(8位)。扇区地址是8位(注意:扇区寄存器里第一个扇区是1扇区,而不是0扇区)。而磁头地址是4位(没有完全占用8位)。因此,硬盘柱面的最大数是65,536(2的16次方),磁头的最大数是 16(2的4次方),扇区的最大数是255(2的8次方-1,注意刚刚我们提到的扇区寄存器问题)。所以,能寻址的最大扇区数是267,386,880 (65,536x16x255)。一个扇区的大小是512字节,也就是说如果以CHS寻址方式,IDE硬盘的最大容量为136.9GB。
在LBA寻址方式中,上述的总共28位可用的寄存器空间(16+8+4)被看作一个完整的LBA地址,因为包括了位0(在CHS模式中扇区不能从0开始计算),其能寻址的扇区数是268,435,456 (65,536x16x256),这时IDE硬盘的最大容量为137.4GB。
特别要指出的是由于BIOS中int13缺陷所导致的528MB和8.4GB限制
早先的硬盘容量比较小,所以在设计BIOS时,在把寻址地址从Int 13的地址寄存器转换为IDE(ATA)的地址寄存器时,仅仅把int13中10位的柱面地址对应IDE(ATA)界面中的16位柱面寄存器,而把没有用到的6位(高位寄存器)地址都设定为0。同时仅把6位的扇区地址来对应IDE(ATA)界面的8位扇区寄存器,其中没有用到的2位设置为0。并且仅使用 int13中的磁头寄存器4位(又去掉了4位)来对应IDE(ATA)。因此,此时的磁盘柱面最大数为1024(2的10次方),磁头的最大数是16(2 的4次方),扇区的最大数是63(2的6次方-1)。所以能寻址的扇区数就成了1,032,192(1,024x16x63)。一个扇区的容量是512字节,也就是说如果以CHS寻址方式,IDE硬盘的最大容量为528.4MB。因此528MB的硬盘容量限制就出现了。
后来尽管EIDE接口对普通IDE接口进行了扩展,支持了LBA存取方式,突破了528MB的容量限制,理论上可以支持到128G的硬盘容量。但老式的BOIS却继续使用10bit表示柱面数,8bit表示磁头数,6bit表示扇区数,因此老式BOIS最多可以支持 8.4GB的容量(512×63×255×1024=8.4GB)。
在目前新设计的BIOS中,新的int13不使用原有的寄存器传递硬盘的寻址参数,它使用的是存储在操作系统内存里的地址包(没有操作系统支持仍然有问题)。地址包里保存的是64位LBA地址,如果硬盘支持LBA寻址,就把低28位直接传递给ATA界面,如果不支持,操作系统就先把LBA地址转换为CHS地址,再传递给ATA界面。通过这种方式,能实现在ATA总线基础上CHS寻址最大容量是136.9GB,而 LBA寻址最大容量是137.4GB。
同时随着ATA-6规范以及48-Bit LBA Adress的规范的实施和发展,再加上ICH4以上南桥的支持,目前早已经突破硬盘所遇到的137.4GB 问题。
Maxtor是推出48-Bit LBA Address规范最早的公司,其中心思想就是增加CHS的位数,在48-Bit LBA Adress规范中,把扇区地址设置为16位的寄存器,磁头的地址寄存器也设为16位,柱面地址寄存器不变。这样在LBA寻址中可用的寄存器空间就从28 位提高到了48位(16+16+16),可以寻址的扇区数就为281,474,976,710,655(65,536x65,535x65,536),整个硬盘的容量就是281,474,976,710,655x512=144,115,188,075,855,872字节,大约等于 144PB(1PB=1000,000,000,000,000字节)。48位LBA寻址基本上就可以支持非常大容量硬盘的寻址了。
2。相关BIOS调用
这里指的BIOS调用主要是int13的相关磁盘功能,有兴趣可以参考中断大全。
2.1。功能0x41
检查磁盘是否支持LBA,例如:
movb $0x41, %ah
movw $0x55aa, %bx
int $0x13
2.2。功能0x42
从指定扇区读数据到内存
%dl可以从功能0x41中获得,是设备号,磁盘为0x80
%ds:%si是指定的内存地址
2.3。功能0x8
获取磁盘参数
2.4。功能0x2
读取指定扇区数据到内存
%al是扇区个数
%ch是柱面号
%cl是扇区号,第6、7位是柱面号高位
%dh是磁头
%dl是设备,0x80是磁盘,0x0是软驱
%es:%bx是指定的内存地址
四。MBR(stage1)详解
* MBR 获取: dd if=/dev/sda of=mbr bs=512 count=1
* MBR 反汇编, nasm工具集中ndisam mbr :
于是利用bview将0x4a之前数据先删掉再反汇编
一个实际启动硬盘的MBR内容,大小为512字节。
下面我们分析MBR的内容。我们使用AT&T汇编语言,通过对MBR的反汇编来解释MBR启动。注意在系统启动时MBR会被BIOS载入到内存的0x7c00位置。
1。启动跳转
00000000h:EB 48 :jmp $0x0000004a ;跳转到0x0000004a位置执行,实际是0x00000048+2(EB 48所占的两个字节)
00000003h:90:nop
2。参数信息
00000004h至0000003dh是BIOS参数块BPB。
0000003eh:03:COMPAT_VERSION_MAJOR版本号
0000003fh:02:COMPAT_VERSION_MINOR版本号
00000040h:FF:GRUB_INVALID_DRIVE,载入stage2标记
00000042h:00 20:start程序载入到的地址0x2000,实际上从这里就可以看出来,此bootloader是支持stage1_5的
00000044h:01 00 00 00:start程序的扇区位置
00000048h:00 02:start程序的段地址0x0200
3。启动磁盘的检查以及载入start程序前的准备
0000004ah:FA :cli ;清中断标记
0000004bh:90 90 :nop nop
0000004dh:F6 C2 80 :testb $0x80, %dl :这是为了避免一些有问题的BIOS没有将启动设备放到%dl中
00000050h:75 02 :jnz $0x00000054 :如果测试为非0,则认为GRUB被安装到软驱上,直接跳转
00000052h:B2 80 :movb $0x80, %dl :如果%dl没有被设置,就将其设置为0x80
00000054h:EA 59 7C 00 00 :ljmp $0x00007c59 ;长跳转到0x7c59,实际上就是这里的0x0059,因为磁盘上的0x0000就对应内存中的0x7c00,使用长跳转是为了避免有问题的BIOS跳转到07c0:0000而不是0000:7c00
00000059h:31 C0 :xorw %ax, %ax
0000005bh:8E D8:movw %ax, %ds
0000005dh:8E D0:movw %ax, %ss;设置%ds和%ss为0
0000005fh:BC 00 20:movw $0x2000, %sp; 设置栈启始从0x2000开始
00000062h:FB:sti;设置中断标志
00000063h:A0 40 7C:movb $(0x7c40), %al ;实际就是0x40处的内容,0x7c40-0x7c00,这里就是0xFF
00000066h:3C FF:cmpb $0xFF, %al ;检查是否有设置了GRUB_INVALID_DRIVE标记,确认%al中是否0xFF
00000068h:74 02:je $0x0000006c :相等的话跳到0x0000006c
0000006ah:88 C2:movb %al, %dl ,将0xFF保存到%dl
0000006ch:52:pushw %dx
0000006dh:BE 7F 7D:movw $(0x7d7f), %si;取0x7d7f-0x7c00=0x17f处的内容 ,当前为GRUB
00000070h:E8 34 01:call $0x01a7;即0x0134+0x70+0x03=0x01a7 ,实际上是调用message过程在屏幕上打印GRUB字样
4。判断磁盘模式,CHS还是LBA
00000073h:F6 C2 80:testb $0x80, %dl ;如果是软驱(0x80)的话就不进行LBA判断
00000076h:74 54:jz $0x00cc;0x76+0x54+0x2=0xcc ,如果比较结果为0,是软驱,直接跳转到CHS模式
00000078h:B4 41:movb $0x41, %ah
0000007ah:BB AA 55:movw $0x55aa, %bx
0000007dh:CD 13:int $0x13 ;调用int13的0x41检查磁盘是否支持LBA模式
0000007fh:5A:popw %dx
00000080h:52:pushw %dx
00000081h:72 49:jc $0x00cc ;出错跳转到CHS模式
00000083h:81 FB 55 AA:cmpw $0xaa55, %bx
00000087h:75 43:jne $0x00cc ;不相等跳转到CHS模式
00000089h:A0 41 7C:$(0x7c41), %al ;取0x0041处内容,是否强制为LBA(grub安装时可以强制LBA),目前为0,不是强制LBA
0000008ch:84 C0:testb %al, %al
0000008eh:75 05:jnz $0x0095 ;若不为0,是强制LBA,跳转到LBA模式
00000090h:83 E1 01:andw $1, %cx
00000093h:74 37:jz $0x00cc ;若为0,跳转到CHS模式,显然在这里为0,所以实际上是进入CHS模式
5。使用LBA模式读取start程序,读取到内存0x7000处
00000095h:66 8B 4C 10:movl 0x10(%si), %ecx ;这里是LBA模式的入口,保存扇区数目到%ecx
00000099h:BE 05 7C:movw $(0x7c05), %si
0000009ch:C6 44 FF 01:movb $1, -1(%si) :设置非零模式
000000a0h:66 8B 1E 44 7C:movl $(0x7c44), %ebx :保存扇区位置到%ebx ,这里为1,实际上就是第2扇区
000000a5h:C7 04 10 00:movw $0x0010, (%si)
000000a9h:C7 44 02 01 00:movw $1, 2(%si)
000000aeh:66 89 5c 08:movl %ebx, 8(%si) ;计算扇区的LBA绝对地址
000000b2h:C7 44 06 00 70:movw $0x7000, 6(%si)
000000b7h:66 31 C0:xorl %eax, %eax
000000bah:89 44 04:movw %ax, 4(%si)
000000bdh:66 89 44 0C:movl %eax, 12(%si)
000000c1h:B4 42:movb $0x42, %ah
000000c3h:CD 13:int $0x13 ;使用int13的功能42将LBA指定的磁盘数据拷贝到0x7000
000000c5h:72 05:jc $0x00cc ;如果出错;则跳转到CHS模式
000000c7h:BB 00 70:movw $0x7000, %bx
000000cah:EB 7D:jmp $0x0149 ;跳转到移动数据到指定位置的调用入口
6。使用CHS模式读取start程序,读取到内存0x7000处
000000cch:B4 08:movb $8, %ah ;这里是CHS模式的入口,int13功能8为获取驱动器参数
000000ceh:CD 13:int $0x13 ;调用BIOS决定磁盘的geometry
000000d0h:74 0A:jnc $0x00dc ;情况正常进入处始化过程
000000d2h:F6 C2 80:testb $0x80, %dl
000000d5h:0F 84 EA 00:jz $0x01c3 ;调用失败,如果%dl为0x80则探测软盘
000000d9h:E9 8D 00:jmp $0x0169 ;否则打印硬盘错误
000000dch:BE 05 7C:movw $(0x7c05), %si ;CHS初始化过程开始
000000dfh:C6 44 FF 00:movb $0, -1(%si) ;设置模式为0
000000e3h:66 31 C0:xorl %eax, %eax ;保存磁头数开始
000000e6h:88 F0:movb %dh, %al
000000e8h:40:incw %ax
000000e9h:66 89 44 04:movl %eax, 4(%si)
000000edh:31 D2:xorw %dx, %dx
000000efh:88 CA:movb %cl, %dl
000000f1h:C1 E2 02:shlw $2, %dx
000000f4h:88 E8:movb %ch, %al
000000f6h:88 F4:movb %dh, %ah ;保存磁头数结束
000000f8h:40:incw %ax ;保存柱面数开始
000000f9h:89 44 08:movw %ax, 8(%si)
000000fch:31 C0:xorw %ax, %ax
000000feh:88 D0:movb %dl, %al
00000100h:C0 E8 02:shrb $2, %al ;保存柱面数结束
00000103h:66 89 04:movl %eax, (%si);保存扇区数
00000106h:66 A1 44 7C:movl $(0x7c44), %eax ;从0x44位置载入逻辑启始扇区地址,这里为1,实际上是就第2扇区
0000010ah:66 31 D2:xorl %edx, %edx ;清0
0000010dh:66 F7 34:divl (%si) ;除以扇区数
00000110h:88 54 0A:movb %dl, 10(%si) ;保存启始扇区
00000113h:66 31 D2:xorl %edx, %edx ;清0
00000116h:66 F7 74 04:divl 4(%si) ;除以磁头数
0000011ah:88 54 0B:movb %dl, 11(%si) ;保存启始磁头
0000011dh:89 44 0C:movw %ax, 12(%si) ;保存启始柱面
00000120h:3B 44 08:cmpw 8(%si), %ax ;柱面是否超出
00000123h:7D 3C:jge $0x0161 ;若大于等于则出Geom错误
00000125h:8A 54 0D:movb 13(%si), %dl ;获取柱面的高位
00000128h:C0 E2 06:shlb $6, %dl ;平移6位
0000012bh:8A 4C 0A:movb 10(%si), %cl ;获取扇区
0000012eh:FE C1:incb %cl
00000130h:08 D1:orb %dl, %cl
00000132h:8A 6C 0C:movb 12(%si), %ch ;将扇区+柱面高位放到cl,将柱面放到ch
00000135h:5A:popw %dx
00000136h:8A 74 0B:movb 11(%si), %dh ;磁头号
00000139h:BB 00 70:movw $0x7000, %bx
0000013ch:8E C3:movw %bx, %es
0000013eh:31 DB:xorw %bx, %bx
00000140h:B8 01 02:movw $0x0201, %ax
00000143h:CD 13:int $0x13 ;int13功能0x2,将指定扇区内容读到0x7000
00000145h:72 24:jc $0x0171 ;磁盘读错误则跳转
7。将start程序从0x7000移动到指定的启始地址位置,在这里是0x2000,并跳转到start程序
00000147h:8C C3:movw %es, %bx
00000149h:8E 06 48 7C:movw $(0x7c48), %es;将0x7000的内容拷贝到0x0048指定的地址,这里是0x0200:0x0000
0000014dh:60:pusha
0000014eh:1E:pushw %ds
0000014fh:B9 00 01:movw $0x100, %cx
00000152h:8E DB:movw %bx, %ds
00000154h:31 F6:xorw %si, %si
00000156h:31 FF:xorw %di, %di
00000158h:FC:cld
00000159h:F3 A5:rep movsw ;串移动
0000015bh:1F:popw %ds
0000015ch:61:popa
0000015dh:FF 26 42 7C:jmp $(0x7c42) ;跳转到0x2000处执行,进入start阶段
8。一些基本的函数调用
00000161h:BE 85 7D:movw $0x7d85, %si ;geometry_error调用
00000164h:E8 40 00:call $0x01a7
00000167h:EB 0E:jmp $0x0177
00000169h:BE 8A 7D:movw $0x7d8a, %si ;hd_probe_error调用
0000016ch:E8 38 00:call $0x01a7
0000016fh:EB 06:jmp $0x0177
00000171h:BE 94 7D:movw $0x7d94, %si ;read_error调用
00000174h:E8 30 00:call $0x01a7
00000177h:BE 99 7D:movw $0x7d99, %si ;general_error调用
0000017ah:E8 2A 00:call $0x01a7
0000017dh:EB FE:jmp $0x017d ;进入死循环
。。。
000001a0h:BB 01 00:movw $0x0001, %bx
000001a3h:B4 0E:movb $0xe, %ah
000001a5h:CD 10:int $0x10
000001a7h:AC:lodsb ;在屏幕上显示消息的调用
000001a8h:3C 00:cmpb $0, %al
000001aah:75 F4:jne $0x01a0
000001ach:C3:ret
下图是对应的未安装到启动硬盘前的原始stage1内容,大小也为512字节。
对比MBR,我们可以看到被修改的地址有:
0x43:80 -> 20 实际上就是stage2的启始原先是0x8000的,在这个实例中,由于支持stage1_5,在安装grub时被setup_func修改成0x2000了。
0x49:08 -> 02 原来是0x0800,现在是0x0200,成为stage1_5的段地址,。
0x4b至0x4c:EB 07 -> 90 90 这是将原来的一个jmp指令改为nop 。
0x1be至0x1fc:实际上包含了分区表信息。
最后大家可以通过直接分析stage1.S文件进一步理解stage1的工作过程。
五。start程序的作用
start位于第2个扇区,在此实例中实际数据如下所示:
在此扇区中,0x01fe开始的内容0x0220是下一次转载到的段地址,0x01fc开始的内容0x000e是 start需要读取的扇区数目,从0x01f8开始的0x00000002是start读取的启始扇区,实际上是第三扇区。在这里我们通过分析 start.S来解析处理过程。
_start:
bootloop:
setup_sectors:
lba_mode:
1:
chs_mode:
2:
copy_buffer:
bootit:
geometry_error:
read_error:
general_error:
stop: jmp stop ;进入死循环
到此为止,start已经把第三扇区后的0x0e个扇区都读入从0x2200开始的内存中了。
六。真正的入口 - EXT_C(main)
EXT_C(main) 在文件asm.S中,就是从地址0x2200开始的入口调用。在这里只做主要流程的分析。
ENTRY(main):
codestart::
在init_bios_info 中调用了stage1_5的cmain,此时已经加载了文件驱动,可以将stage2通过文件系统方式读入到地址0x8000处,然后执行ENTRY(chain_stage2)。下面看文件stage2/stage1_5.c中的cmain。
grub_open (config_file);打开stage2文件
grub_read ((char *) 0x8000, SECTOR_SIZE * 2);读取2个扇区的内容到地址0x8000
ret = grub_read ((char *) 0x8000 + SECTOR_SIZE * 2, -1);读取其余数据
chain_stage2 (0, 0x8200, saved_sector);具体函数在文件stage2/asm.S中
在ENTRY(chain_stage2)中,首先是EXT_C(prot_to_real)退出保护模式,最后跳转到从地址0x8200处开始执行,实际上就是跳过start,再次进入EXT_C(main)。
movl 0x8(%esp), %eax;取出栈中第一个参数(%esp+8)的内容放到%eax中
movl %eax, offset;实际上是将第一个参数0放到offset
movl %eax, %ebx
movw 0x4(%esp), %ax;取出栈中第二个参数(%esp+4)的内容放到%ax中
movw %ax, segment;实际上就是0x8200
shll $4, %eax;左移4位,得到0x0820
addl %eax, %ebx;产生线性地址0x0820:0000
movl 0xc(%esp), %ecx;将saved_sector赋给%ecx
call EXT_C(prot_to_real);从保护模式进入实模式
DATA32 ADDR32 ljmp (offset);跳转到0x0820:0000,即进入stage2的EXT_C(main)
第二次进入EXT_C(main),前面执行的内容和第一次进入一样,只不过这一次cmain不是上一次stage1_5的cmain了,真正进入了stage2的cmain,即grub的交互处理循环了,主要步骤如下:
run_menu;处理用户键盘指令和用户选择菜单的命令,如光标上下移动、修改启动参数、选择启动选项等。
run_script;处理用户选择的启动选项中的命令,如root、kernel、initrd等命令,注意最后系统会自己加上boot命令。
builtin->func;具体执行root_func、kernel_func、initrd_func和boot_func命令。
七。kernel_func - 载入内核
在grub的stage2中的文件builtins.c中有一个结构builtin_table,是所有grub 支持命令的函数对应表,其中设计内核启动的主要有kernel_func和boot_func,另外setup_func是设计bootloader安装的处理,在这里也做介绍。
kernel_func 是将内核载入到内存指定地址的处理。
首先指定内核参数地址。
mb_cmdline = (char *) MB_CMDLINE_BUF;MB_CMDLINE_BUF=0x2000
grub_memmove (mb_cmdline, arg, len + 1);;将内核参数移动到0x2000
load_image (arg, mb_cmdline, suggested_type, load_flags);开始栽入内核
在load_image中使用了文件系统读取fsys_table,这里就不详细介绍了。
grub_open (kernel);打开内核文件,在这里即bzImage文件
grub_read (buffer, MULTIBOOT_SEARCH);读取开头MULTIBOOT_SEARCH=8192个字节到buffer ,如下图所示部分内容:
lh = (struct linux_kernel_header *) buffer;;在这里lh是linux_kernel_header结构指针,具体可以参考Linux启动协议的定义
这时候一定是lh->boot_flag == BOOTSEC_SIGNATURE && lh->setup_sects <= LINUX_MAX_SETUP_SECTS;BOOTSEC_SIGNATURE=0xAA55,LINUX_MAX_SETUP_SECTS=64,分别见偏移0x1fe和0x1f1,lh->setup_sects=0x0a
int setup_sects = lh->setup_sects;内核setup部分占的扇区数目,在这里就是0x0a
lh->type_of_loader = LINUX_BOOT_LOADER_TYPE;指定type_of_loader为LINUX_BOOT_LOADER_TYPE=0x71
linux_data_real_addr = (char *) ((mbi.mem_lower << 10) - LINUX_SETUP_MOVE_SIZE);LINUX_SETUP_MOVE_SIZE=0x9100,mbi.mem_lower是系统低位内存大小,一般为640k
if (linux_data_real_addr > (char *) LINUX_OLD_REAL_MODE_ADDR)
linux_data_real_addr = (char *) LINUX_OLD_REAL_MODE_ADDR;LINUX_OLD_REAL_MODE_ADDR=0x90000 ;如果linux_data_real_addr 大于0x90000,则实际数据地址不能超过0x90000
lh->heap_end_ptr = LINUX_HEAP_END_OFFSET;设置heap_end_ptr ,LINUX_HEAP_END_OFFSET=0x9000 - 0x200
lh->loadflags |= LINUX_FLAG_CAN_USE_HEAP;设置loadflags,LINUX_FLAG_CAN_USE_HEAP=0x80
lh->cmd_line_ptr = linux_data_real_addr + LINUX_CL_OFFSET;设置cmd_line_ptr,内核即参数位置,LINUX_CL_OFFSET=0x9000
data_len = setup_sects << 9;获得bzImage中实模式代码setup部分的大小,这里是0x0a<<9,即0x1400字节
text_len = filemax - data_len - SECTOR_SIZE;;获得bzImage其余部分,即保护模式代码的大小
linux_data_tmp_addr = (char *) LINUX_BZIMAGE_ADDR + text_len;设置临时指针到地址0x100000+保护模式代码尺寸之后
grub_memmove (linux_data_tmp_addr, buffer, MULTIBOOT_SEARCH);将开始时候读取buffer的内容放到0x100000+保护模式代码之后,即将bootsect和setup代码开头部分放到0x100000+保护模式代码之后
grub_read (linux_data_tmp_addr + MULTIBOOT_SEARCH, data_len + SECTOR_SIZE - MULTIBOOT_SEARCH);将实模式代码读全了
char *src = skip_to (0, arg);
char *dest = linux_data_tmp_addr + LINUX_CL_OFFSET;将内核参数拷贝到0x100000+保护模式代码尺寸+0x9000后
while (dest < linux_data_tmp_addr + LINUX_CL_END_OFFSET && *src)
*(dest++) = *(src++);最多拷贝0xff个字节,到0x90FF,所以bootsect+setup到内核参数结束总共为0x9100字节
grub_seek (data_len + SECTOR_SIZE);重新将文件指针定位到保护模式代码
grub_read ((char *) LINUX_BZIMAGE_ADDR, text_len);将保护模式代码拷贝到0x100000
到这里,我们就可以了解到grub将内核载入后的内容地址分布图了:
0x100000开始,是内核保护模式以后代码
0x100000+保护模式代码尺寸开始,是内核bootsec和实模式setup部分代码,在这里bootsect为512字节,setup为0x1400字节
0x100000+保护模式代码尺寸+0x9000开始,是内核参数命令,一共0xff个字节
八。boot_func - 启动内核
boot_func是grub启动内核时的操作,其对内核内容的数据又做了一些修改。
big_linux_boot 位于asm.S文件中,主要操作如下:
1。调整内核bootsect和setup实模式数据位置
将linux_data_tmp_addr(地址0x100000)处实模式代码移到linux_data_real_addr(地址0x90000),移动尺寸大小为LINUX_SETUP_MOVE_SIZE=0x9100,这样把参数也移过去了。
在load_image里已经指出linux_data_real_addr最大为LINUX_OLD_REAL_MODE_ADDR=0x90000 ,这样内核的实际内容地址分布又成了:
0x90000开始,是内核bootsect和实模式setup的执行代码
0x90000+0x9000开始,是内核参数,共0xff个字节
0x100000开始,是内核保护模式代码
2。填写要跳转到的段地址
movl EXT_C(linux_data_real_addr), %ebx ;%ebx为0x90000
shrl $4, %ebx
movl %ebx, %eax
addl $0x20, %eax ;%eax为0x9020
movl %eax, linux_setup_seg;这样下面要跳转的linux_setup_seg地址是就是linux_data_real_addr+ 0x200的段地址,平移4位即段地址0x9020:0000,同时跳过了bootsect的0x200字节,直接执行到setup实模式代码
3。返回实模式
call EXT_C(prot_to_real) ;在EXT_C(main) 中已经介绍,在stage2中进入保护模式,这里又回到实模式,因为内核启始部分还是实模式代码
4。设置内核栈,跳转到内核setup实模式
linux_setup_seg:
可以看到linux_setup_seg是上面的0x9020段地址,这样跳转到的就是0x9020:0000即0x90200。
九。setup_func - 安装grub
安装grub时最关键的是修改了stage1和stage2里的一些内容,具体操作在install_func中。修改的内容主要有:
1。修改stage1中一些参数
*((unsigned char *) (stage1_buffer + STAGE1_BOOT_DRIVE)) = new_drive;设置启动设备
*((unsigned char *) (stage1_buffer + STAGE1_FORCE_LBA)) = is_force_lba;设置是否强制LBA
*((unsigned long *) (stage1_buffer + STAGE1_STAGE2_SECTOR)) = stage2_first_sector;设置stage1_5或stage2启始扇区号
*((unsigned short *) (stage1_buffer + STAGE1_STAGE2_ADDRESS)) = installaddr;设置stage1_5或stage2的载入地址,前者0x2000,后者为0x8000
*((unsigned short *) (stage1_buffer + STAGE1_STAGE2_SEGMENT)) = installaddr >> 4;载入的段地址
2。修改stage2中一些参数
*((unsigned char *) (stage2_second_buffer + STAGE2_FORCE_LBA)) = is_force_lba;设置是否强制LBA
十。grub在内存中的映射表
0 to 4K-1
0x07BE to 0x07FF
down from 8K-1
0x2000 to ?
0x2000 to 0x7FFF
0x7C00 to 0x7DFF
0x7F00 to 0x7F42
0x8000 to ?
The end of Stage 2 to 416K-1
down from 416K-1
416K to 448K-1
448K to 479.5K-1
479.5K to 480K-1
480K to 512K-1
The last 1K of lower memory
磁盘交换代码和数据一。获得可运行的Linux内核
当我们从获得Linux源码并正确编译后,在源码根目录下会生成文件vmlinux,同时在arch/i386/boot/目录下会生成bzImage文件。下面我们看看vmlinux和bzImage分别是如何得到的。没有特殊说明,本系列中Linux的参考对象都为版本2.6.22。
1。vmlinux的获得
vmlinux是Linux源码编译后未压缩的内核,我们查看源码根目录下的.vmlinux.cmd文件,可以看到:
cmd_vmlinux := ld -m elf_i386 -m elf_i386 -o vmlinux -T arch/i386/kernel/vmlinux.lds arch/i386/kernel/head.o arch/i386/kernel/init_task.o init/built-in.o --start-group usr/built-in.o arch/i386/kernel/built-in.o arch/i386/mm/built-in.o arch/i386/mach-default/built-in.o arch/i386/crypto/built-in.o kernel/built-in.o mm/built-in.o fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.o lib/lib.a arch/i386/lib/lib.a lib/built-in.o arch/i386/lib/built-in.o drivers/built-in.o sound/built-in.o arch/i386/pci/built-in.o net/built-in.o --end-group .tmp_kallsyms2.o
这说明vmlinux是由arch/i386/kernel/head.o和arch/i386/kernel /init_task.o以及各个相关子目录下的built-in.o链接而成的。注意按照链接顺序我们可以发现arch/i386/kernel /head.S的目标文件似乎比较靠前。
2。bzImage的获得
bzImage是内核的压缩版本,一般可以是vmlinux大小的三分之一左右。
首先查看生成bzImage的链接文件arch/i386/boot/.bzImage.cmd
cmd_arch/i386/boot/bzImage := arch/i386/boot/tools/build -b arch/i386/boot/bootsect arch/i386/boot/setup arch/i386/boot/vmlinux.bin CURRENT > arch/i386/boot/bzImage
接下去根据线索我们查看生成vmlinux.bin的链接文件arch/i386/boot/.vmlinux.bin.cmd
cmd_arch/i386/boot/vmlinux.bin := objcopy -O binary -R .note -R .comment -S arch/i386/boot/compressed/vmlinux arch/i386/boot/vmlinux.bin
然后查看生成vmlinux的链接文件arch/i386/boot/compressed/.vmlinux.cmd
cmd_arch/i386/boot/compressed/vmlinux := ld -m elf_i386 -m elf_i386 -T arch/i386/boot/compressed/vmlinux.lds arch/i386/boot/compressed/head.o arch/i386/boot/compressed/misc.o arch/i386/boot/compressed/piggy.o -o arch/i386/boot/compressed/vmlinux
接下去查看生成piggy.o的链接文件arch/i386/boot/compressed/.piggy.o.cmd
cmd_arch/i386/boot/compressed/piggy.o := ld -m elf_i386 -m elf_i386 -r --format binary --oformat elf32-i386 -T arch/i386/boot/compressed/vmlinux.scr arch/i386/boot/compressed/vmlinux.bin.gz -o arch/i386/boot/compressed/piggy.o
然后接下去查看生成vmlinux.bin.gz的链接文件arch/i386/boot/compressed/.vmlinux.bin.gz.cmd
cmd_arch/i386/boot/compressed/vmlinux.bin.gz := gzip -f -9 < arch/i386/boot/compressed/vmlinux.bin > arch/i386/boot/compressed/vmlinux.bin.gz
最后我们查看生成vmlinux.bin的链接文件arch/i386/boot/compressed/.vmlinux.bin.cmd,注意这里的vmlinux就是根目录下的vmlinux。
cmd_arch/i386/boot/compressed/vmlinux.bin := objcopy -O binary -R .note -R .comment -S vmlinux arch/i386/boot/compressed/vmlinux.bin
下面我们将生成bzImage的过程总结一下:
a。由vmlinux文件strip掉符号表得到arch/i386/boot/compressed/vmlinux.bin
b。将vmlinux.bin压缩成vmlinux.bin.gz
c。将vmlinux.scr和vmlinux.bin.gz链接成piggy.o
d。将head.o、misc.o和piggy.o链接成当前目录下的vmlinux
e。将vmlinux文件strip掉符号表得到arch/i386/boot/vmlinux.bin
f。将bootsect、setup和vmlinux.bin拼接成bzImage
二。内核装载时的内存空间映射
下面是文件Documentation/i386/boot.txt中提供的bzImage在内存中的映射图,和本实例略有出入,下面我们会指出,但基本描述了bzImage在内存中的分布情况。
下图是传统的Image或zImage内存映射图
结合上一章在bootloader中boot_func所讲的实际情况,内核在内存中的地址映射应该是这样的:
0x100000以上:内核保护模式代码
0x99000-0x99100:内核参数命令
0x90000-0x99000:内核bootsect和setup实模式代码,bootsect大小512字节,setup0x1400字节
0x9000开始:内核栈地址
三。内核启始相关文件分析
从以上bzImage的生成过程,我们可以发现,arch/i386/boot/bootsect和arch /i386/boot/setup应该是做初始工作的,接下来应该是arch/i386/boot/compressed/head.o,然后可能就是 vmlinux是由arch/i386/kernel/head.o。那么我们就按照顺序从arch/i386/boot/bootsect.S开始分析。
下面图片是bzImage的前0x240个字节内容。
四。arch/i386/boot/bootsect.S
bootsect.S生成的文件bootsect大小只有512字节,也就是上图中的0x0000到0x01ff的内容,是不是有点眼熟,其实里面另有玄机。下面我们来看bootsect.S的内容。
_start:
start2:
msg_loop:
die:
从这里可以看出,此处内核的bootsect其实没有任何意义,实际上在2.6版本的linux中,必须要有另外的bootloader才能启动内核,例如grub。在前面我们分析grub的boot_func中的big_linux_boot里,描述了实际上 grub的stage2将内核的bootsect和setup实模式代码载入到地址0x90000后,是skip了头0x200个字节的,直接跳转到地址 0x90200处执行的。
五。arch/i386/boot/setup.S
setup.S是真正内核的开始,上面图片从0x200开始就是setup的内容。
从0x0202开始的4个字节是特征值"HdrS"。
0x206开始的内容0x0206是版本号,其实是Linux内核头协议号。
0x20c开始的内容是SYSSEG,即系统载入的段地址0x1000。
接下来是kernel_version内容的偏移量,在这里是0x11b8,实际上就是setup的启始地址 0x200+0x11b8=0x13b8,在这里因为太长没有给出图片,可以告诉大家实际内容是"2.6.22 ( root@FG4DEV ) #6 SMP Thu Aug 2 16:57:24 CST 2007"。
0x211内容为1,指出此内核为big-kernel。
0x212开始的内容是0x8000,代表setup_move_size的大小,后面将会遇到。
0x214的内容代表了内核将要加载到的地址,在这里是0x100000。
从0x240到0xeff是E820和EDD的保留空间。
下面我们介绍主要流程。
start:
trampoline:
start_of_setup:
1。检查特征值
good_sig1:
good_sig:
2。检查是否载入的是big-kernel
3。检查cpu情况
loader_ok:
1:
cpu_ok:
4。获取内存大小,在这里共使用了3种不同方式检测内存:通过e820h方式获取内存地图,通过e801h方式获得32位内存尺寸,最后通过88h获得0-64m 。有关e820h可以访问获得ACPI 2.0规范的详细内容
下面的e820h方式
meme820:
jmpe820:
good820:
again820:
bail820:
下面是e801h方式
meme801:
e801usecxdx:
这里是88h方式,最古老的方式,难道最后内容放在地址0x02中?
mem88:
5。设置键盘敲击速率到最大
6。检查显示设备参数并设置模式,具体看arch/i386/boot/video.S,这里不做介绍了
7。获取hd0数据
8。获取hd1数据
9。检查是否有hd1
no_disk1:
is_disk1:
10。检查微通道总线MCA,IBM提出的早期总线,目前一般系统都不带MCA总线了
sysdesc_ok:
no_mca:
11。检测PS/2点设备
no_psmouse:
12。准备进入保护模式
rmodeswtch_normal:
rmodeswtch_end:
13。将系统移到正确的位置,如果是big-kernel我们就不移动了
do_move0:
do_move:
end_move:
14。载入段地址,确认bootloader是否支持启动协议版本2.02,决定是否需要移动代码到0x90000 ,关于启动协议可以参考Documentation/i386/boot.txt ,本实例中是不需要移动的
move_self_1:
move_self_here:
end_move_self:
15。打开A20 ,A20地址线是一个历史遗留问题,早期为了使用1M以上内存而使用的开关,目前一般硬件缺省就是打开的
a20_try_loop:
a20_none:
a20_bios:
a20_kbc:
a20_kbc_wait:
a20_kbc_wait_loop:
a20_fast:
a20_fast_wait:
a20_fast_wait_loop:
a20_die:
a20_done:
16。设置gdt、idt和32位的启动地址
17。复位所有可能存在的协处理器
18。屏蔽所有中断
19。真正进入保护模式,跳转到arch/i386/boot/compressed/head.S中的startup_32
flush_instr:
code32:
.word BOOT_CS ;要跳转的代码段地址BOOT_CS=GDT_ENTRY_BOOT_CS * 8=2*8
startup_32:
1:
jmpl *(code32_start - start + (DELTA_INITSEG << 4))(%esi) ;这里跳转到arch/i386/boot/compressed/head.S里的startup_32
20。初始化时第一次设定的gdt和idt
gdt:
gdt_end:
idt_48:
gdt_48:
六。arch/i386/boot/compressed/head.S
arch/i386/boot/compressed/head.S负责将压缩的内核解压缩,并跳转到解压后的内核执行,主要流程如下:
1。段地址准备,在内核里都为BOOT_DS=GDT_ENTRY_BOOT_DS*8=(GDT_ENTRY_BOOT_CS + 1)*8=(2+1)*8
2。拷贝压缩的内核到缓存结尾以保证安全
3。计算内核启始地址
4。将压缩内核解压
call decompress_kernel
5。跳转到解压后的内核执行
xorl %ebx,%ebx
jmp *%ebp
七。arch/i386/kernel/head.S
arch/i386/kernel/head.S是真正的32位启动代码。
1。段地址准备
2。内核启动参数准备
3。初始化页面表
4。设置idt
5。检查cpu类型
6。跳转到start_kernel
jmp start_kernel
八。start_kernel
开始进入C语言的启动流程,其中一些内存管理、设备初始化、调度等相关细节将在后续章节详细介绍,这里只是简要叙述基本流程。
1。初始化tick控制
2。页面地址表page_address_maps和page_address_htable初始化
3。初始化内核代码、数据段并计算页面数
4。设置内存页面表映射
5。初始化调度
6。建立zonelists
7。设置系统trap调用
8。设置系统中断调用
9。初始化pidhash
10。设置时钟软中断TIMER_SOFTIRQ调用
11。设置高分辨率时钟软中断HRTIMER_SOFTIRQ调用
12。设置软中断TASKLET_SOFTIRQ和HI_SOFTIRQ的调用
13。初始化终端设备
14。初始化dcache和inode
15。内核空间内存地址分配
16。初始化slab机制
17。优先树结构index_bits_to_maxindex初始化
18。初始化fork机制
19。基树结构height_to_maxindex初始化
20。初始化信号机制
21。初始化acpi
22。启动第一个内核线程kernel_init
九。第一个内核线程 - kernel_init
内核最后的初始化,准备开始进入第一个应用层程序。
1。初始化工作队列
2。设备框架初始化
例如一些需要满足sys文件系统的初始化,bus、class等
3。初始化所有的initcalls
initcalls机制在2.6早期版本是不完全的,例如把网络相关的sock_init仍然以调用方式初始化,现今的内核版本已经把所有的子系统都改为以do_initcalls的形式初始化了。
4。运行第一个应用程序
打开系统终端,依次寻找/sbin/init、/etc/init、/bin/init和/bin/sh执行,若都没有成功,则打印错误信息,挂起系统。
十。参考资料