一。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 于是利用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内核 当我们从www.kernel.org获得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可以访问www.acpi.info获得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:
|