Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3310478
  • 博文数量: 258
  • 博客积分: 9440
  • 博客等级: 少将
  • 技术积分: 6998
  • 用 户 组: 普通用户
  • 注册时间: 2009-05-03 10:28
个人简介

-- linux爱好者,业余时间热衷于分析linux内核源码 -- 目前主要研究云计算和虚拟化相关的技术,主要包括libvirt/qemu,openstack,opennebula架构和源码分析。 -- 第五届云计算大会演讲嘉宾 微博:@Marshal-Liu

文章分类

全部博文(258)

文章存档

2016年(1)

2015年(4)

2014年(16)

2013年(22)

2012年(41)

2011年(59)

2010年(40)

2009年(75)

分类: 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:

  • pushw %dx
  • pushw %si
  • MSG(notification_string) ;打印"Loading stage1.5"信息到屏幕
  • popw %si
  • movw $ABS(firstlist - BOOTSEC_LISTSIZE), %di ;获取接下来要读取的磁盘扇区号,保存在偏移位置0x01f8,这里的firstlist是start的末尾地址,定义为8,这里%di值为0x2,实际上就是第三扇区
  • movl (%di), %ebp

bootloop:

  • cmpw $0, 4(%di) ;4(%di)是0x01fc,具体数据在这里是0x000e,代表的是还要读的扇区数目,这里检查读完所有扇区没有
  • je bootit ;读完了就跳转到bootit了

setup_sectors:

  • cmpb $0, -1(%si) ;检查是LBA还是CHS模式
  • je chs_mode

lba_mode:

  • movl (%di), %ebx ;获取启始扇区号
  • xorl %eax, %eax
  • movb $0x7f, %al ;最大读取扇区数目不超过0x7f,这是由于Phoenix EDD的限制
  • cmpw %ax, 4(%di) ;看看需要读取的扇区数目是否大于0x7f
  • jg 1f ;大于的话,跳到1:
  • movw 4(%di), %ax ;小于就赋需要读取的扇区数目给%ax

1:

  • subw %ax, 4(%di) ;将%ax内值减去需要读取的扇区数目
  • addl %eax, (%di) ;加上启始扇区号
  • movw $0x0010, (%si) ;保留空间
  • movw %ax, 2(%si)
  • movl %ebx, 8(%si) ;计算绝对扇区地址(低32位)
  • movw $BUFFERSEG, 6(%si) ;设置读取到的内存地址
  • pushw %ax xorl %eax, %eax
  • movw %ax, 4(%si)
  • movl %eax, 12(%si) ;计算绝对扇区地址(高32位)
  • movb $0x42, %ah
  • int $0x13 ;使用int13的功能0x42读取扇区数据
  • jc read_error ;读取错误则报错
  • movw $BUFFERSEG, %bx
  • jmp copy_buffer ;跳转到数据拷贝

chs_mode:

  • movl (%di), %eax
  • xorl %edx, %edx
  • divl (%si)
  • movb %dl, 10(%si)
  • xorl %edx, %edx
  • divl 4(%si)
  • movb %dl, 11(%si)
  • movw %ax, 12(%si)
  • cmpw 8(%si), %ax
  • jge geometry_error ;geometry错误处理
  • movw (%si), %ax
  • subb 10(%si), %al
  • cmpw %ax, 4(%di)
  • jg 2f
  • movw 4(%di), %ax

2:

  • subw %ax, 4(%di)
  • addl %eax, (%di)
  • movb 13(%si), %dl
  • shlb $6, %dl
  • movb 10(%si), %cl
  • incb %cl
  • orb %dl, %cl
  • movb 12(%si), %ch
  • popw %dx
  • pushw %dx
  • movb 11(%si), %dh
  • pushw %ax
  • movw $BUFFERSEG, %bx ;要将扇区数据读到的内存地址0x7000
  • movw %bx, %es
  • xorw %bx, %bx
  • movb $0x2, %ah
  • int $0x13 ;读取扇区数据到内存
  • jc read_error ;读错误处理
  • movw %es, %bx

copy_buffer:

  • movw 6(%di), %es ;6(%di)内容在0x01fe,值是0x0220,是要将数据载入到的段地址
  • popw %ax
  • shlw $5, %ax
  • addw %ax, 6(%di)
  • pusha
  • pushw %ds
  • shlw $4, %ax
  • movw %ax, %cx
  • xorw %di, %di
  • xorw %si, %si
  • movw %bx, %ds
  • cld
  • rep
  • movsb ;拷贝数据
  • popw %ds
  • MSG(notification_step) ;打印信息
  • popa
  • cmpw $0, 4(%di)
  • jne setup_sectors ;看是否读取完所有扇区
  • subw $BOOTSEC_LISTSIZE, %di
  • jmp bootloop

bootit:

  • MSG(notification_done) ;打印结束信息
  • popw %dx
  • ljmp $0, $0x2200 ;跳转到准备好的入口

geometry_error:

  • MSG(geometry_error_string) ;打印错误信息"Geom"
  • jmp general_error

read_error:

  • MSG(read_error_string) ;打印错误信息"Read"

general_error:

  • MSG(general_error_string) ;打印错误信息"Error"

stop: jmp stop ;进入死循环

到此为止,start已经把第三扇区后的0x0e个扇区都读入从0x2200开始的内存中了。

六。真正的入口 - EXT_C(main)

EXT_C(main) 在文件asm.S中,就是从地址0x2200开始的入口调用。在这里只做主要流程的分析。

ENTRY(main):

  • ljmp $0, $ABS(codestart)

codestart::

  • cli ;清中断
  • xorw %ax, %ax
  • movw %ax, %ds
  • movw %ax, %ss
  • movw %ax, %es ;设置%ds、%ss和%es
  • movl $STACKOFF, %ebp ;设置实模式栈
  • movl %ebp, %esp
  • sti ;开中断
  • DATA32 call EXT_C(real_to_prot) ;转换实模式到保护模式
  • subl %edi, %ecx ;计算bss长度
  • xorb %al, %al
  • cld
  • rep
  • stosb
  • call EXT_C(init_bios_info) ;从这里开始就进入到c语言的代码调用了

在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实模式

  • movw %bx, %ss;注意此时%bx是0x9000
  • movw $LINUX_SETUP_STACK, %sp;LINUX_SETUP_STACK=0x9000
  • movw %bx, %ds
  • movw %bx, %es
  • movw %bx, %fs
  • movw %bx, %gs;将所有段地址赋值,0x9000:0000
  • byte 0xea
  • word 0

linux_setup_seg:

  • word 0

可以看到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

  • BIOS和实模式中断

0x07BE to 0x07FF

  • 可以传递到另外的bootloader的分区表

down from 8K-1

  • 实模式使用的栈

0x2000 to ?

  • stage1_5载入的启始地址

0x2000 to 0x7FFF

  • 多启动内核以及模块的命令行缓存

0x7C00 to 0x7DFF

  • BIOS或其它bootloader将stage1载入的启始地址

0x7F00 to 0x7F42

  • LBA设备参数

0x8000 to ?

  • stage2载入的启始地址

The end of Stage 2 to 416K-1

  • stage2菜单使用的堆

down from 416K-1

  • 保护模式使用的栈

416K to 448K-1

  • 文件系统缓存

448K to 479.5K-1

  • Raw设备缓存

479.5K to 480K-1

  • 512-byte扩展空间

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:

  • jmpl $BOOTSEG, $start2 ;这里的BOOTSEG就是段地址0x07C0,使用一个长跳转到start2

start2:

  • movw %cs, %ax
  • movw %ax, %ds
  • movw %ax, %es
  • movw %ax, %ss
  • movw $0x7c00, %sp
  • sti
  • cld
  • movw $bugger_off_msg, %si ;bugger_off_msg中的信息为"Direct booting from floppy is no longer supported.\r\nPlease use a boot loader program instead.\r\n\nRemove disk and press any key to reboot . . .\r\n"

msg_loop:

  • lodsb
  • andb %al, %al
  • jz die ;打印完成后跳转到die
  • movb $0xe, %ah
  • movw $7, %bx
  • int $0x10 ;使用int10打印信息到屏幕
  • jmp msg_loop

die:

  • xorw %ax, %ax
  • int $0x16 ;允许用户按任意一键重启
  • int $0x19
  • ljmp $0xf000,$0xfff0 ;一般上面的中断调用后不会到这里了,如果有例外情况,直接跳转到BIOS的重启代码

从这里可以看出,此处内核的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:

  • jmp trampoline

trampoline:

  • call start_of_setup

start_of_setup:

  • movw $0x01500, %ax
  • movb $0x81, %dl
  • int $0x13

1。检查特征值

  • movw %cs, %ax ;此时cs代码段地址为SETUPSEG,即0x9020
  • movw %ax, %ds
  • cmpw $SIG1, setup_sig1 ;检查偏移地址setup_sig1处内容是否为0xAA55 ,这一般在编译生成setup时就写好了
  • jne bad_sig
  • cmpw $SIG2, setup_sig2 ;检查偏移地址setup_sig2处内容是否为0x5A5A
  • jne bad_sig
  • jmp good_sig1 ;检查特征值没问题

good_sig1:

  • jmp good_sig

good_sig:

  • movw %cs, %ax
  • subw $DELTA_INITSEG, %ax ;这里DELTA_INITSEG = SETUPSEG - INITSEG = 0x9020 - 0x9000 = 0x0020
  • movw %ax, %ds

2。检查是否载入的是big-kernel

  • testb $LOADED_HIGH, %cs:loadflags ;检查是否big-kernel,实际就是看bzImage的0x211处的值是否为1,在本实例中是big-kernel ,将会被加载到高位0x100000,从bzImage的0x214开始的内容也可以看出。
  • jz loader_ok
  • cmpb $0, %cs:type_of_loader ;确认是否有loader可以处理接下来的工作,在这里是没有的,值为0,在偏移地址0x210处
  • jnz loader_ok
  • pushw %cs
  • popw %ds
  • lea loader_panic_mess, %si
  • call prtstr;打印"Wrong loader, giving up..."
  • jmp no_sig_loop;挂起,进入死循环

3。检查cpu情况

loader_ok:

  • call verify_cpu ;具体在arch/i386/kernel/verify_cpu.S中,这里就不做详细介绍了
  • testl %eax,%eax
  • jz cpu_ok
  • movw %cs,%ax
  • movw %ax,%ds
  • lea cpu_panic_mess,%si
  • call prtstr ;打印"PANIC: CPU too old for this kernel."

1:

  • jmp 1b ;进入死循环

cpu_ok:

4。获取内存大小,在这里共使用了3种不同方式检测内存:通过e820h方式获取内存地图,通过e801h方式获得32位内存尺寸,最后通过88h获得0-64m 。有关e820h可以访问获得ACPI 2.0规范的详细内容

下面的e820h方式

  • xorl %eax, %eax
  • movl %eax, (0x1e0)
  • movb %al, (E820NR) ;E820NR=0x1e8

meme820:

  • xorl %ebx, %ebx
  • movw $E820MAP, %di ;E820MAP=0x2d0

jmpe820:

  • movl $0x0000e820, %eax
  • movl $SMAP, %edx ;SMAP就是"SMAP"
  • movl $20, %ecx
  • pushw %ds
  • popw %es
  • int $0x15
  • jc bail820
  • cmpl $SMAP, %eax
  • jne bail820

good820:

  • movb (E820NR), %al
  • cmpb $E820MAX, %al ;E820MAX=128,即E820MAP实例的个数
  • jae bail820 ;128个后获得后跳出循环,内容都在地址0x1e8 开始的128个实例中
  • incb (E820NR)
  • movw %di, %ax
  • addw $20, %ax
  • movw %ax, %di

again820:

  • cmpl $0, %ebx
  • jne jmpe820

bail820:

下面是e801h方式

meme801:

  • stc
  • xorw %cx,%cx
  • xorw %dx,%dx ;据说这是为了避免有问题的BIOS产生错误
  • movw $0xe801, %ax
  • int $0x15
  • jc mem88
  • cmpw $0x0, %cx
  • jne e801usecxdx
  • cmpw $0x0, %dx
  • jne e801usecxdx
  • movw %ax, %cx
  • movw %bx, %dx

e801usecxdx:

  • andl $0xffff, %edx
  • shll $6, %edx
  • movl %edx, (0x1e0)
  • andl $0xffff, %ecx
  • addl %ecx, (0x1e0) ;内容放到地址0x1e0中

这里是88h方式,最古老的方式,难道最后内容放在地址0x02中?

mem88:

  • movb $0x88, %ah
  • int $0x15
  • movw %ax, (2)

5。设置键盘敲击速率到最大

  • movw $0x0305, %ax
  • xorw %bx, %bx
  • int $0x16

6。检查显示设备参数并设置模式,具体看arch/i386/boot/video.S,这里不做介绍了

  • call video

7。获取hd0数据

  • xorw %ax, %ax
  • movw %ax, %ds
  • ldsw (4 * 0x41), %si
  • movw %cs, %ax
  • subw $DELTA_INITSEG, %ax
  • pushw %ax
  • movw %ax, %es
  • movw $0x0080, %di
  • movw $0x10, %cx
  • pushw %cx
  • cld
  • rep
  • movsb

8。获取hd1数据

  • xorw %ax, %ax
  • movw %ax, %ds
  • ldsw (4 * 0x46), %si
  • popw %cx
  • popw %es
  • movw $0x0090, %di
  • rep
  • movsb

9。检查是否有hd1

  • movw $0x01500, %ax
  • movb $0x81, %dl
  • int $0x13
  • jc no_disk1
  • cmpb $3, %ah
  • je is_disk1

no_disk1:

  • movw %cs, %ax
  • subw $DELTA_INITSEG, %ax
  • movw %ax, %es
  • movw $0x0090, %di
  • movw $0x10, %cx
  • xorw %ax, %ax
  • cld
  • rep
  • stosb

is_disk1:

10。检查微通道总线MCA,IBM提出的早期总线,目前一般系统都不带MCA总线了

  • movw %cs, %ax
  • subw $DELTA_INITSEG, %ax
  • movw %ax, %ds
  • xorw %ax, %ax
  • movw %ax, (0xa0)
  • movb $0xc0, %ah
  • stc
  • int $0x15
  • jc no_mca
  • pushw %ds
  • movw %es, %ax
  • movw %ax, %ds
  • movw %cs, %ax
  • subw $DELTA_INITSEG, %ax
  • movw %ax, %es
  • movw %bx, %si
  • movw $0xa0, %di
  • movw (%si), %cx
  • addw $2, %cx
  • cmpw $0x10, %cx
  • jc sysdesc_ok
  • movw $0x10, %cx

sysdesc_ok:

  • rep
  • movsb
  • popw %ds

no_mca:

11。检测PS/2点设备

  • movw %cs, %ax
  • subw $DELTA_INITSEG, %ax
  • movw %ax, %ds
  • movb $0, (0x1ff)
  • int $0x11
  • testb $0x04, %al
  • jz no_psmouse
  • movb $0xAA, (0x1ff) ;设备存在

no_psmouse:

12。准备进入保护模式

  • cmpw $0, %cs:realmode_swtch ;在这里realmod_swtch实际上是0,所以跳转到rmodeswtch_normal
  • jz rmodeswtch_normal
  • lcall *%cs:realmode_swtch
  • jmp rmodeswtch_end

rmodeswtch_normal:

  • pushw %cs
  • call default_switch ;default_switch调用的实际操作是在真正进入保护模式前关闭中断并禁止NMI

rmodeswtch_end:

13。将系统移到正确的位置,如果是big-kernel我们就不移动了

  • testb $LOADED_HIGH, %cs:loadflags
  • jz do_move0
  • jmp end_move

do_move0:

  • movw $0x100, %ax
  • movw %cs, %bp
  • subw $DELTA_INITSEG, %bp
  • movw %cs:start_sys_seg, %bx
  • cld

do_move:

  • movw %ax, %es
  • incb %ah
  • movw %bx, %ds
  • addw $0x100, %bx
  • subw %di, %di
  • subw %si, %si
  • movw $0x800, %cx
  • rep
  • movsw
  • cmpw %bp, %bx
  • jb do_move

end_move:

14。载入段地址,确认bootloader是否支持启动协议版本2.02,决定是否需要移动代码到0x90000 ,关于启动协议可以参考Documentation/i386/boot.txt ,本实例中是不需要移动的

  • movw %cs, %ax
  • movw %ax, %ds
  • cmpl $0, cmd_line_ptr ;检查是否需要向下兼容小于2.01的bootloader ,在此例中cmd_line_ptr是0,为版本2.02以上
  • jne end_move_self
  • cmpb $0x20, type_of_loader
  • je end_move_self
  • movw %cs, %ax ;bootloader不支持2.02协议,如果代码段不在0x90000,需要将其移动到0x90000
  • cmpw $SETUPSEG, %ax ;SETUPSEG=0x9020
  • je end_move_self
  • cli
  • subw $DELTA_INITSEG, %ax ;DELTA_INITSEG=0x0020
  • movw %ss, %dx
  • cmpw %ax, %dx
  • jb move_self_1
  • addw $INITSEG, %dx ;INITSEG=0x9000
  • subw %ax, %dx

move_self_1:

  • movw %ax, %ds
  • movw $INITSEG, %ax
  • movw %ax, %es
  • movw %cs:setup_move_size, %cx
  • std
  • movw %cx, %di
  • decw %di
  • movw %di, %si
  • subw $move_self_here+0x200, %cx
  • rep
  • movsb
  • ljmp $SETUPSEG, $move_self_here

move_self_here:

  • movw $move_self_here+0x200, %cx
  • rep
  • movsb
  • movw $SETUPSEG, %ax
  • movw %ax, %ds
  • movw %dx, %ss

end_move_self:

15。打开A20 ,A20地址线是一个历史遗留问题,早期为了使用1M以上内存而使用的开关,目前一般硬件缺省就是打开的

a20_try_loop:

a20_none:

  • call a20_test ;先直接看看是否成功,万一系统就不需要打开A20,直接跳转就可以了
  • jnz a20_done

a20_bios:

  • movw $0x2401, %ax
  • pushfl
  • int $0x15 ;尝试使用int15设置A20
  • popfl
  • call a20_test ;看看是否成功
  • jnz a20_done

a20_kbc:

  • call empty_8042 ;尝试通过键盘控制器设置A20
  • call a20_test ;看看是否成功
  • jnz a20_done
  • movb $0xD1, %al
  • outb %al, $0x64
  • call empty_8042
  • movb $0xDF, %al
  • outb %al, $0x60 ;开启A20命令
  • call empty_8042

a20_kbc_wait:

  • xorw %cx, %cx

a20_kbc_wait_loop:

  • call a20_test ;看看是否成功
  • jnz a20_done
  • loop a20_kbc_wait_loop

a20_fast:

  • inb $0x92, %al;最后的尝试,通过配置Port A
  • orb $0x02, %al
  • andb $0xFE, %al
  • outb %al, $0x92

a20_fast_wait:

  • xorw %cx, %cx

a20_fast_wait_loop:

  • call a20_test ;看看是否成功
  • jnz a20_done
  • loop a20_fast_wait_loop
  • decb (a20_tries) ;尝试了a20_tries=A20_ENABLE_LOOPS=255次
  • jnz a20_try_loop
  • movw $a20_err_msg, %si
  • call prtstr;仍然没有效果,打印"linux: fatal error: A20 gate not responding!"

a20_die:

  • hlt
  • jmp a20_die;打开A20失败,进入死循环

a20_done:

16。设置gdt、idt和32位的启动地址

  • lidt idt_48
  • xorl %eax, %eax
  • movw %ds, %ax
  • shll $4, %eax
  • addl %eax, code32 ;设置32位启动地址,修改code32指定的地址,将增加了代码段后的数据写入code32指定的内存地址中
  • addl $gdt, %eax
  • movl %eax, (gdt_48+2) ;将下面将介绍gdt的地址写到gdt_48+2指定的地址中
  • lgdt gdt_48

17。复位所有可能存在的协处理器

  • xorw %ax, %ax
  • outb %al, $0xf0
  • call delay
  • outb %al, $0xf1
  • call delay

18。屏蔽所有中断

  • movb $0xFF, %al
  • outb %al, $0xA1
  • call delay
  • movb $0xFB, %al
  • outb %al, $0x21;又打开了中断2,因为irq2是cascaded

19。真正进入保护模式,跳转到arch/i386/boot/compressed/head.S中的startup_32

  • movw $1, %ax
  • lmsw %ax ;真正进入保护模式
  • jmp flush_instr

flush_instr:

  • xorw %bx, %bx
  • xorl %esi, %esi
  • movw %cs, %si
  • subw $DELTA_INITSEG, %si
  • shll $4, %esi
  • .byte 0x66, 0xea ;这里实际上是硬写入代码指令66 ea,即进入到保护模式下的长跳转

code32:

  • long startup_32 ;跳转到startup_32
  • .word BOOT_CS ;要跳转的代码段地址BOOT_CS=GDT_ENTRY_BOOT_CS * 8=2*8

startup_32:

  • movl $(BOOT_DS), %eax ;数据段地址BOOT_DS=GDT_ENTRY_BOOT_DS * 8=(GDT_ENTRY_BOOT_CS + 1)*8=(2+1)*8
  • movl %eax, %ds
  • movl %eax, %es
  • movl %eax, %fs
  • movl %eax, %gs
  • movl %eax, %ss ;在启动时的保护模式里,设置内核里的地址段都为代码段地址
  • xorl %eax, %eax

1:

  • incl %eax
  • movl %eax, 0x00000000
  • cmpl %eax, 0x00100000
  • je 1b ;检查A20是否开启,如果没开启就一直死循环
  • jmpl *(code32_start - start + (DELTA_INITSEG << 4))(%esi) ;这里跳转到arch/i386/boot/compressed/head.S里的startup_32

20。初始化时第一次设定的gdt和idt

  • align 16

gdt:

  • fill GDT_ENTRY_BOOT_CS,8,0
  • .word 0xFFFF ;4Gb - (0x100000*0x1000 = 4Gb)
  • .word 0 ;基地址为0
  • .word 0x9A00 ;代码段的属性是可读可执行
  • .word 0x00CF
  • .word 0xFFFF ;4Gb - (0x100000*0x1000 = 4Gb)
  • .word 0 ;基地址为0
  • .word 0x9200 ;数据段的属性是可读可写
  • .word 0x00CF

gdt_end:

  • align 4
  • word 0

idt_48:

  • word 0
  • word 0, 0
  • word 0

gdt_48:

  • word gdt_end - gdt - 1 ;gdt的尺寸
  • word 0, 0 ;gdt的地址,在运行时才填写实际的绝对地址

六。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执行,若都没有成功,则打印错误信息,挂起系统。

十。参考资料


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