bootloader详解(转载)

一。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内核

当我们从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:

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可以访问www.acpi.info获得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执行,若都没有成功,则打印错误信息,挂起系统。

十。参考资料

http://www.linux.org/docs/ldp/howto/Linux-i386-Boot-Code-HOWTO/index.html

下面我们迎来了真正的起点(start_of_setup),主要流程为:

  1. 复位硬盘控制器
  2. 如果 %ss 无效,重新计算栈指针
  3. 初始化栈,开中断
  4. 将 cs 设置为 ds,与 setup.elf 的入口地址一致
  5. 检查主引导扇区末尾标志,如果不正确则跳到 setup_bad
  6. 清空 bss 段
  7. 跳到 main(定义在 boot/main.c)

目录

  1. Linux 引导过程综述
  2. BIOS
    • POST
    • 自举过程
  3. Boot loader
    • 主引导扇区结构
    • GRUB stage1
    • GRUB stage2
  4. 内核初始化:体系结构相关部分
    • 内核映像结构
    • header.S
    • 初始化与保护模式
    • 自解压内核
    • startup_32
  5. 内核初始化:体系结构无关部分
    • 核心数据结构初始化
    • 设备初始化

1 Linux引导过程综述

  1. BIOS
    在 i386 平台中,由 BIOS 作最初的引导工作,执行加电自检、初始化,读取引导设备的主引导扇区并执行。
  2. Boot loader(以 GRUB 为例)
    MBR 中的、紧随 MBR 后的 phase 1/1.5 boot loader 载入文件系统中的 phase 2 及其配置,显示操作系统选择菜单,执行用户命令,载入选定的操作系统内核与 initrd。
  3. 内核初始化:体系结构相关部分
    从 header.S 开始,到 main.c 初始化参数,再到 pm.c 进入保护模式,然后载入 vmlinuz 并自解压,在 startup_32.S 中开启分页机制、初始化中断向量表、检测 CPU 类型等,完成 x86 体系结构的保护模式初始化。这是本文重点。
  4. 内核初始化:体系结构无关部分
    分为核心数据结构初始化(start_kernel)和设备初始化两个阶段。
  5. 用户态初始化
    以下内容超出了本文范围。用户态的 init 程序:
    • 获取运行信息
    • 执行 /etc/rc[runlevel].d 中的启动脚本
    • 加载内核模块(/etc/modprobe.conf)
    • 执行 /etc/init.d 中的脚本
    • 执行 /bin/login,等待用户登录
    • 接受 shell 中的用户控制

2 BIOS

BIOS的主要功能概括来说包括如下几部分:

  • POST

    加电自检,检测 CPU 各寄存器、计时芯片、中断芯片、DMA 控制器等

  • Initial

    枚举设备,初始化寄存器,分配中断、IO 端口、DMA 资源等

  • Setup

    进行系统设置,存于 CMOS 中。

  • 常驻程序

    INT 10h、INT 13h、INT 15h 等,提供给操作系统或应用程序调用。

  • 启动自举程序

    在POST过程结束后,将调用 INT 19h,启动自举程序,自举程序将读取引导记录,装载操作系统。

BIOS 的启动主要由 POST 过程与自举过程构成。

2.1 POST

当 PC 加电后,CPU 的寄存器被设为某些特定值。其中,指令指针寄存器(program counter)被设为 0xfffffff0。

CR1,一个32位控制寄存器,在刚启动时值被设为0。CR1 的 PE (Protected Enabled,保护模式使能) 位指示处理器是处于保护模式还是实模式。由于启动时该位为0,处理器在实模式中引导。在实模式中,线性地址与物理地址是等同的。

在实模式下,0xfffffff0 不是一个有效的内存地址,计算机硬件将这个地址指向 BIOS 存储块。这个位置包含一条跳转指令,指向 BIOS 的 POST 例程。

POST(Power On Self Test,加电自检)过程包括内存检查、系统总线检查等。如果发现问题,主板会蜂鸣报警。在 POST 过程中,允许用户选择引导设备。

POST 的最后一步是执行 INT 0x19 指令,开始自举过程。

POST 过程在 AWARD BIOS 的源码中在 BOOTROM.ASM 文件中 BootBlock_POST 函数过程中实现,主要步骤如下:

  1. 初始化各种主板芯片组
  2. 初始化键盘控制器
  3. 初始化中断向量、中断服务例程
  4. 初始化 VGA BIOS 控制器
  5. 显示 BIOS 的版本和公司名称
  6. 扫描各种介质容量并显示
  7. 读取 CMOS 的启动顺序配置
  8. 调用 INT 0x19 启动自举程序

2.2 自举过程

自举过程即为执行中断 INT 0x19 的中断服务例程 INT19_VECT 的过程 (Bootrom.asm)

主要功能为读取引导设备第一个扇区的前 512 字节(MBR),将其读入到内存 0x0000:7C00,并跳转至此处执行。

3 Boot loader

3.1 主引导扇区结构

硬盘第一个扇区的前 512 个字节是主引导扇区,由 446 字节的 MBR、64 字节的分区表和 2 字节的结束标志组成。

  • MBR(Master Boot Record)是 446 字节的引导代码,被 BIOS 加载到 0x00007C00 并执行。
  • 硬盘分区表占据主引导扇区的 64 个字节(0x01BE -- 0x01FD),可以对四个分区的信息进行描述,其中每个分区的信息占据 16 个字节。

    一个分区记录有如下域:

    • 1字节 文件系统类型
    • 1字节 可引导标志
    • 6字节 CHS格式描述符
    • 8字节 LBA格式描述符

    LBA和CHS两种描述符指示相同的信息,但是指示方式有所不同:LBA (逻辑块寻址,Logical Block Addressing)指示分区的起始扇区和分区长度, 而CHS(柱面 磁头 扇区)指示首扇区和末扇区。

  • 结束标志字 55,AA(0x1FEH -- 0x1FFH)是主引导扇区的最后两个字节,是检验主引导记录是否有效的标志。

3.2 GRUB stage1

Linux 的启动方式包括 LILO、GRUB 等。这里结合 GRUB 源代码分析其引导过程。

GRUB 的引导过程分为 stage1、stage 1.5 和 stage 2。其中 stage1 和可能存在的 stage1.5 是为 stage2 做准备,stage2 像一个微型操作系统。

  1. BIOS 加载 GRUB stage1(如果安装到 MBR)到 0x00007C00.

  2. stage1 位于 stage1/stage1.S,汇编后形成 512 字节的二进制文件,写入硬盘的0面0道第1扇区。

    stage1 将0面0道第2扇区上的 512 字节读到内存中的0x00007000处,然后调用 COPY_BUFFER 将其拷贝到 0x00008000 的位置上,然后跳至 0x00008000 执行。这 512 字节代码来自 stage2/start.S,作用是 stage1_5 或者 stage2(编译时决定加载哪个)的加载器。

    /* start.S */
    blocklist_default_start:
    .long 2	 /* 从第3扇区开始*/
    blocklist_default_len:
    /* 需要读取多少个扇区 */
    #ifdef STAGE1_5
    .word 0	 /* 如果是 STAGE1_5,则不读入 */
    #else
    .word (STAGE2_SIZE + 511) >> 9 /* 读入 Stage2 所占的所有扇区 */
    #endif
    blocklist_default_seg:
    #ifdef STAGE1_5
    .word 0x220 /* 将 stage1.5 加载到 0x2200 */
    #else
    .word 0x820	/* 将 stage2 加载到 0x8200 */
    #endif
    
  3. 由于 stage1 和 start 不具备文件系统识别功能,stage 1.5 只能被存放在固定的扇区中。例如 e2fs_stage1_5 就被存放在0面0道第3扇区开始的一段连续空间里。(第一个主分区是从1面0道第1扇区开始的,stage 1.5 不会覆盖主分区内容)

    stage 1.5 能够读取文件系统,负责从文件系统中载入并执行 stage 2,即 GRUB 的核心映像。由于系统引导过程中不需要修改文件系统,因此只实现了文件系统的读取。

    可以说,stage 1.5 是 stage 1 与 stage 2 之间的桥梁,解决了文件系统这个“先有鸡还是先有蛋”的问题。

3.3 GRUB stage2

stage2 将系统切换到保护模式,设置 C 运行环境,寻找 config 文件,执行 shell 接受用户命令,载入选定的操作系统内核。

  1. stage2 的入口点是 asm.s
    #ifdef STAGE1_5
    # define	ABS(x)	((x) - EXT_C(main) + 0x2200)
    #else
    # define	ABS(x)	((x) - EXT_C(main) + 0x8200)
    #endif
    
    1. 初始化一些变量
    2. 跳转到 code_start
    3. 关中断,设置段寄存器和堆栈起始地址
    4. 从实模式切换到保护模式
    5. 清空 bss 段
    6. init_bios_info()
  2. 随后进入 stage2.c,执行 GRUB 的主要功能。

    • cmain(): 主函数,载入配置文件 menu.lst(GRUB 1)或 grub.cfg(GRUB 2),如果成功载入就进入 run_menu(),显示菜单,进入循环倒计时,如果超时就进入第一个,如果用户按了键就停止倒计时。用户作出选择后,跳转到 boot_entry(),清空屏幕、获取入口,通过 find_command 找到的函数指针调用相应的命令。

    • 如果没有成功载入配置文件,就 enter_cmdline(),也是通过 find_command 调用相应的命令。

  3. 每个 GRUB 命令都要在 stage2/builtin.c 的 builtin_table 数组中登记:
    struct builtin
    {
        char *name;			/* 命令名称 */
        int (*func) (char *, int);	/* 命令执行时调用的函数指针 */
        int flags;			/* 标志,似乎未用到 */
        char *short_doc;		/* 短帮助 */
        char *long_doc;		/* 详细帮助 */
    };
    struct builtin *builtin_table[];
    
  4. 常用 GRUB 命令:
    • root:挂载分区并设为根分区。
      root_func (char *arg, int flags)
    • kernel:对传进来的参数逐个解析,获得 linux 内核映像路径,通过 load_image() 载入内核。
      kernel_func (char *arg, int flags)
    • boot:根据操作系统类型调用不同的启动函数,将控制权转交给操作系统。支持 BSD、linux、chain loader、multi boot 等方式。
      boot_func (char *arg, int flags)
  5. stage2 中的文件系统驱动:

    每种文件系统都要按照 stage2/filesys.h 的定义在 stage2/disk_io.c 的 fsys_table 数组中登记:

    /* stage2/filesys.h */
    struct fsys_entry
    {
        char *name;                                         //文件系统名称
        int (*mount_func) (void);                           //挂载
        int (*read_func) (char *buf, int len);              //读文件
        int (*dir_func) (char *dirname);                    //打开文件
        void (*close_func) (void);                          //关闭文件
        int (*embed_func) (int *start_sector, int needed_sectors);  //不清楚
    };
    

    GRUB 调用 grub_open() 打开文件。grub_open 在 fsys_table 数组中逐个调用 fsys_entry::mount_func(),找到当前已挂载的文件系统,再用 fsys_entry::dir_func() 方法打开文件。

4 内核初始化:体系结构相关部分

4.1 内核映像结构

根据 Linux/I386 启动协议(Documentation/i386/boot.txt),x86 体系结构大内核内存使用如下:

For a modern bzImage kernel with boot protocol version >= 2.02, a
memory layout like the following is suggested:

        ~                        ~   
        |  Protected-mode kernel |
100000  +------------------------+
        |  I/O memory hole       |   
0A0000  +------------------------+
        |  Reserved for BIOS     |      Leave as much as possible unused
        ~                        ~   
        |  Command line          |      (Can also be below the X+10000 mark)
X+10000 +------------------------+
        |  Stack/heap            |      For use by the kernel real-mode code.
X+08000 +------------------------+    
        |  Kernel setup          |      The kernel real-mode code.
        |  Kernel boot sector    |      The kernel legacy boot sector.
X       +------------------------+
        |  Boot loader           |      <- Boot sector entry point 0000:7C00
001000  +------------------------+
        |  Reserved for MBR/BIOS |
000800  +------------------------+
        |  Typically used by MBR |
000600  +------------------------+ 
        |  BIOS use only         |   
000000  +------------------------+

根据 arch/x86/boot/Makefile,bzImage 大内核映像由 setup.elf 和 vmlinux 组成,而 vmlinux 又由 setup.bin 和 vmlinux.bin 组成。vmlinux.bin 会进行压缩存储,变成 vmlinux.bin.gz。因此 bzImage 由 setup.elf、setup.bin、vmlinux.bin.gz 三部分组成。

Line 28: targets         := vmlinux.bin setup.bin setup.elf zImage bzImage
Line 29: subdir-         := compressed
Line 30: 
Line 31: setup-y         += a20.o cmdline.o copy.o cpu.o cpucheck.o edd.o
Line 32: setup-y         += header.o main.o mca.o memory.o pm.o pmjump.o
Line 33: setup-y         += printf.o string.o tty.o video.o video-mode.o version.o

其中 setup-y 就是 setup.elf,其中引用的 header.o 是从 header.S 汇编而来的。

Line 77: $(obj)/bzImage: IMAGE_OFFSET := 0x100000
Line 86: $(obj)/zImage $(obj)/bzImage: $(obj)/setup.bin \
Line 87:                               $(obj)/vmlinux.bin $(obj)/tools/build FORCE
Line 88:         $(call if_changed,image)
Line 89:         @echo 'Kernel: $@ is ready' ' (#'`cat .version`')'
Line 90:
Line 91: OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S

大内核情况下的内存分布图:

        |  vmlinux               |   
100000  +------------------------+
        |  setup.elf的setup部分   |
090200  +------------------------+
        |  setup.elf的启动扇区     |
090000  +------------------------+
        |  BootLoader            |
007c00  +------------------------+
        |                        |
000000  +------------------------+

在进入源代码的世界之前,我们先看看用于控制 arch/x86/boot 下代码进行链接的 setup.ld。

ld 文件用于控制 ld 的链接过程:

  • 描述输入文件的各节如何对应到输出文件的各节
  • 控制输入文件各节及符号的内存布局

每个对象文件有一个节(section)列表、一个符号列表,一个符号可以是已定义或未定义的。每个已定义的符号有地址。未定义的符号则要在链接时从其他文件中寻找其定义。

  1. 指定输出文件格式
    OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
  2. 指定目标体系结构
    OUTPUT_ARCH(i386)
  3. 设置入口点
    ENTRY(_start)
  4. 输入文件各节到输出文件的映射
    SECTIONS
    {
    . = 0				// 从 0 开始
    .bstext : { *(.bstext) }	// 所有输入文件的 .bstext 节组合成输出文件的 .bstext 节
    .bsdata : { *(.badata) }	// 所有输入文件的 .bsdata 节...
    . = 497				// 填充 512 字节的 bootloader(见4.2节 header.S)
    .header : { *(.header) }
    

    在每一部分(header、rodata、data、bss、end)之间,对齐 16 字节内存边界:

    . = ALIGN(16);

    最后用断言保证链接后的目标文件不太大,且偏移量正确。

4.2 header.S

start2:
	movw	%cs, %ax        # CS = 0x7c00
	movw	%ax, %ds	# 初始化段寄存器
	movw	%ax, %es
	movw	%ax, %ss
	xorw	%sp, %sp
	sti			# 开中断
	cld			# di++, si++
................................
msg_loop:			# 打印字符例程
................................
bs_die:				# 错误处理例程
        .ascii  "Direct booting from floppy is no longer supported.\r\n"
        .ascii  "Please use a boot loader program instead.\r\n"
        .ascii  "\n"
        .ascii  "Remove disk and press any key to reboot . . .\r\n"
        .byte   0

这段代码编译链接后,会生成 512 字节的 bootsector,其中 .section ".header", "a" 中的变量共 15 字节。注意到 setup.ld (Linker script for the i386 setup code) 中加入了 497 字节的空白,事实上恰好凑够 512 字节。

事实上,上一节我们提到,MBR 是由 GRUB 写入的,因此这里的 bootsector 对于硬盘启动是用不到的。GRUB 等 boot loader 将 setup.elf 读到 0x90000 处,将 vmlinux 读到 0x100000 处,然后跳转到 0x90200 开始执行,恰好跳过了 512 字节的 bootsector。

有意思的是,从软盘启动时,header.S 生成的 bootsector 做的惟一一件事就是打印错误信息(bs_die),不支持从软盘启动。

下面就是 0x90200(_start)了,目的就是跳到 start_of_setup。

         # Part 2 of the header, from the old setup.S
................................
# End of setup header #####################################################

上面这两行之间的代码是一个庞大的数据结构,与 include/asm/bootparam.h 中的 struct setup_header 一一对应。这个数据结构定义了启动时所需的默认参数,其中一些参数可以通过命令选项 overwrite。下表列出了一些参数的意义。

名称偏移大小(字节)意义
root_flags0x1f22根目录是否只读,可用 ro 或 rw 选项指定
root_dev0x1fc2默认的 root 设备,即 /boot 所在目录,可用 root= 选项指定
boot_flag0x1fe20xAA55,即主引导扇区结束标志
header0x2024HdrS (0x53726448),内核标志
version0x2062启动协议版本号: major * 64 + minor
kernel_version0x20e2内核版本号
type_of_loader0x2101Boot loader ID: Boot loader ID * 64 + Version No.
Boot loader IDs:
0 LILO
1 Loadlin
2 bootsect-loader
3 SYSLINUX
4 EtherBoot
5 ELILO
7 GRuB
8 U-BOOT
9 Xen
A Gujin
B Qemu
loadflags0x2111启动选项的掩码。
  • Bit 0: LOADED_HIGH (1表示保护模式代码加载到 0x100000)
  • Bit 7: CAN_USE_HEAP (为1表示 heap_end_ptr 有效)
code32_start0x2144内核解压缩前立即跳转到的 32 位 flat-mode 入口
ramdisk_image0x2184initramfs 的 32 位线性地址
cmd_line_ptr0x2284内核命令行的 32 位线性地址

下面我们迎来了真正的起点(start_of_setup),主要流程为:

  1. 复位硬盘控制器
  2. 如果 %ss 无效,重新计算栈指针
  3. 初始化栈,开中断
  4. 将 cs 设置为 ds,与 setup.elf 的入口地址一致
  5. 检查主引导扇区末尾标志,如果不正确则跳到 setup_bad
  6. 清空 bss 段
  7. 跳到 main(定义在 boot/main.c)

4.3 初始化与保护模式

我们终于暂时离开了汇编代码,走进 “主要” 的启动部分。这一部分在 arch/x86/boot/main.c 中。

main() 中的几个函数调用都有比较详细的注释,主要作用是初始化 boot_params,将来会经常被用到。

include/asm/bootparam.h 中定义的 boot_params 结构体 (即 zeropage) 在此完成初始化:

  • copy_boot_params() 初始化 boot_params.hdr (将 hdr 复制过来)
  • detect_memory() 初始化 boot_params.e820_map 和 boot_params.e820_entries
  • query_apm_bios() 初始化 apm_bios_info、screen_info

go_to_protected_mode() 进入保护模式,代码在 boot/pm.c。

  1. realmode_switch_hook():boot_params.hdr 中有 realmode_swtch,记录了 hook 函数地址,如果有的话就执行之
  2. reset_coprecessor(): 重启协处理器
  3. make_all_interrupts(): 关闭所有旧 PIC 上的中断。其中的 io_delay 等待 I/O 操作完成。
  4. setup_idt(): 初始化中断描述符表 (空的)
  5. setup_gdt(): 初始化 GDT:
    • GDT_ENTRY_BOOT_CS
    • GDT_ENTRY_BOOT_DS
    • GDT_ENTRY_BOOT_TSS

其中 GDT_ENTRY_BOOT_CS 和 GDT_ENTRY_BOOT_DS 基地址都为零,段限长都是 4G。

下面是 GDT 数据结构示意:

http://home.ustc.edu.cn/~boj/courses/linux_kernel/images/2_1.png

  1. protected_mode_jump(): 汇编代码,下面分析。传参说明:进入保护模式后将采用段访问内存地址,因此要将传入的参数转换为线性地址。

下面进入 boot/pmjump.S 中的 protected_mode_jump。

 29 protected_mode_jump:

 30         movl    %edx, %esi              # Pointer to boot_params table

 31

 32         xorl    %ebx, %ebx

 33         movw    %cs, %bx                # 将实模式的代码段放入 bx

 34         shll    $4, %ebx                # 转换为线性地址

 35         addl    %ebx, 2f                # in_pm32 的实模式地址转换为线性地址

 36

 37         movw    $__BOOT_DS, %cx         # ds 段选择子

 38         movw    $__BOOT_TSS, %di        # tss 段选择子

 39

 40         movl    %cr0, %edx

 41         orb     $X86_CR0_PE, %dl        # Protected mode

 42         movl    %edx, %cr0              # cr0 0位置0是进入保护模式的标志

 43         jmp     1f                      # Short jump to serialize on 386/486

 44 1:

 45         # 下面这段作用是跳转到 in_pm32,由于已经在保护模式,所以需要考虑段的问题

 46         # Transition to 32-bit mode

 47         .byte   0x66, 0xea              # ljmpl opcode

 48 2:      .long   in_pm32                 # offset

 49         .word   __BOOT_CS               # segment

 50

 51         .size   protected_mode_jump, .-protected_mode_jump

 52

 53         .code32

 54         .type   in_pm32, @function

 55 in_pm32:        # 下面的注释挺清楚,就不翻译了

 56         # Set up data segments for flat 32-bit mode

 57         movl    %ecx, %ds

 58         movl    %ecx, %es

 59         movl    %ecx, %fs

 60         movl    %ecx, %gs

 61         movl    %ecx, %ss

 62         # The 32-bit code sets up its own stack, but this way we do have

 63         # a valid stack if some debugging hack wants to use it.

 64         addl    %ebx, %esp

 65

 66         # Set up TR to make Intel VT happy

 67         ltr     %di                     # 这个比较有意思

 68

 69         # Clear registers to allow for future extensions to the

 70         # 32-bit boot protocol

 71         xorl    %ecx, %ecx

 72         xorl    %edx, %edx

 73         xorl    %ebx, %ebx

 74         xorl    %ebp, %ebp

 75         xorl    %edi, %edi

 76

 77         # Set up LDTR to make Intel VT happy

 78         lldt    %cx                     # 又是一个骗 CPU 的东西

 

 79         # eax protected_mode_jump 的第一个参数,即 header.S 中定义的 boot_params.hdr.code32_start,即 vmlinux 的入口地址

 80         jmpl    *%eax                   # Jump to the 32-bit entrypoint

 81

 82         .size   in_pm32, .-in_pm32

4.4 自解压内核

上节末尾的 jmpl 指令把我们带入了 vmlinux 的世界。注意到,vmlinux 是压缩存储的,因此内核首先的工作就是把真正的内核解压出来。

根据 Makefile,linux 内核文件有以下几种:

  • vmlinux: 原始的 linux 内核
  • zImage: 经过 gzip 压缩后的 vmlinux,解压到 640KB 内存位置
  • bzImage: 大内核版的 zImage,解压到 1MB 内存位置,现在我们一般都用这个
  • vmlinuz: 指向 zImage 或 bzImage 的链接
  • initrd: init ram disk,用于引导 vmlinuz

循着 Makefile 的踪迹,我们找到了 arch/x86/boot/compressed/head_32.S,这就是大内核模式下 0x100000 开始的内存内容。

  1. 找到 vmlinux 的入口地址,并将其存入 ebp。
  2. 如果设置了可重入内核,就将 ebp 按照 kernel_alignment 对齐,放入 ebx。
  3. 确定解压内核的内存地址
  4. 设置栈
  5. 将 vmlinux 复制到安全地区(ebx 指定的地方):保存 esi 到栈中,首先计算出需要复制的字节数目,然后4个字节为一组地复制过去,再从栈中恢复 esi。
  6. 进入 relocated,清空 BSS,初始化解压函数所用的栈
  7. 将 decompress_kernel 所用的参数入栈:内核加载地址、内核长度、压缩内核安全地址、堆地址、启动参数结构体指针。
  8. 调用 decompress_kernel 解压内核
  9. 如果设置了可重入内核,进行一些 relocate
  10. 跳转到解压后的内核。

至此,arch/x86/boot 下的流程基本分析完毕。

4.5 startup_32

vmlinux 是从哪里来的呢?不知道是否是 Linus 有意为我们增加难度 (其实是我对 make 不熟悉),生成 vmlinux 的命令在源码根目录的隐藏文件 .vmlinux.cmd 中。

md_vmlinux := ld -m elf_i386 --build-id -o vmlinux -T arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_32.o arch/x86/kern

el/head32.o arch/x86/kernel/init_task.o  init/built-in.o --start-group  usr/built-in.o  arch/x86/mach-generic/built-in.o

arch/x86/kernel/built-in.o  arch/x86/mm/built-in.o  arch/x86/mach-default/built-in.o  arch/x86/crypto/built-in.o  arch/x86

/vdso/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/x86/lib/lib.a  lib/built-in.o  arch/x86/lib/built-in.o  drivers/built-in.o  sound/built

-in.o  arch/x86/pci/built-in.o  arch/x86/oprofile/built-in.o  arch/x86/power/built-in.o  net/built-in.o --end-group .tmp_k

allsyms2.o

真正的内核入口是 arch/x86/kernel/head_32.S (为什么也叫 head_32.S?)

汇编函数 startup_32 依次完成以下动作:

  1. 初始化参数
    • 初始化 GDT。boot_gdt_descr 在数据区中记载了 GDT 表首地址。

lgdt pa(boot_gdt_descr)

    • 清空 BSS 段
    • 复制实模式中的 boot_params 结构体
    • 复制命令行参数到 boot_command_line (供 init/main.c 使用)
    • 有关虚拟环境的一些配置
  1. 开启分页机制

尽管我们已经在保护模式中,但只有段机制而没有启用页机制。这里设置全局页目录与页表项,并开启分页机制。

下图示意了 Linux 的分页机制(From ULK)。

http://home.ustc.edu.cn/~boj/courses/linux_kernel/images/2_2.png

    • 如果启用了 PAE,即物理地址扩展到 64G 的机制,不作分析。
    • 不然,就是通常的 4G 线性地址空间。__PAGE_OFFSET 是内核编译时配置的内核地址空间偏移,默认为 3G。默认配置下,进程的用户态地址空间为 0~3G,高 1G 是内核地址空间。

全局页目录大小为 4KB,每项大小为 4B,可以表示 4MB 的线性范围,因此页目录的大小是 __PAGE_OFFSET >> 20。

page_pde_offset = (__PAGE_OFFSET >> 20);

    • 初始化页表首地址 %edi、全局页目录地址 %edx、PTE 属性(页目录和页表的每项 4 Byte 中后 12 位是属性,这里预先填充 0x67)
    • 230         movl $pa(pg0), %edi
    • 231         movl $pa(swapper_pg_dir), %edx
    • 232         movl $PTE_ATTR, %eax
    • 下面是一个双层循环,外层循环填充页目录,内层循环填充页表。
    • 233 10:
    •            # %edi: 页表首地址
    • 234         leal PDE_ATTR(%edi),%ecx                /* Create PDE entry */
    •            # 将页目录项填充到页目录中,%edx 为页目录地址
    • 235         movl %ecx,(%edx)                        /* Store identity PDE entry */
    • 236         movl %ecx,page_pde_offset(%edx)         /* Store kernel PDE entry */
    •            # 填充下一个页目录项
    • 237         addl $4,%edx
    • 238         movl $1024, %ecx
    • 239 11:           # 内层循环,填充 4KB PTD
    • 240         stosl                         # es:edi= eax,edi++
    •            # 表面上看是将 0x1000 加到属性上,事实上是 %eax 的后 12 位属性不变,前面的 20 位页地址加 1
    • 241         addl $0x1000,%eax
    •            # 继续内层循环
    • 242         loop 11b
    • 243         /*
    • 244          * End condition: we must map up to and including INIT_MAP_BEYOND_END
    • 245          * bytes beyond the end of our own page tables; the +0x007 is
    • 246          * the attribute bits
    • 247          */
    •            # 计算何时应停止
    • 248         leal (INIT_MAP_BEYOND_END+PTE_ATTR)(%edi),%ebp
    •            # 如果 %eax < %ebp,继续外层循环
    • 249         cmpl %ebp,%eax
    • 250         jb 10b
    • 添加页目录项的最后一项,页表地址为 swapper_pg_fixmap,用于 fixmap area
    • 251         movl %edi,pa(init_pg_tables_end)
    • 252
    • 253         /* Do early initialization of the fixmap area */
    • 254         movl $pa(swapper_pg_fixmap)+PDE_ATTR,%eax
    • 255         movl %eax,pa(swapper_pg_dir+0xffc)
    • 有关对称多处理器(SMP)的处理
    • 一些 CPU 参数相关的判断和处理
    • 开启分页机制
    •            # 将页表首地址(swapper_pg_dir)放入 cr3
    • 331         movl $pa(swapper_pg_dir),%eax
    • 332         movl %eax,%cr3          /* set the page table pointer.. */
    •            # 设置 cr0 paging 位,打开 cr0 的分页机制
    • 333         movl %cr0,%eax
    • 334         orl  $X86_CR0_PG,%eax
    • 335         movl %eax,%cr0          /* ..and set paging (PG) bit */
    •            # 目前已经开启分页机制,完全进入保护模式。
    • 336         ljmp $__BOOT_CS,$1f     /* Clear prefetch and normalize %eip */
  1. 初始化 Eflags
  2. 初始化中断向量表

在实模式中,已经初始化了 IDT,不过现在我们要对保护模式再做一次这样的工作。由于这段代码比较长,放在了单独的函数里。

485 setup_idt:

         # 默认中断处理例程,后面有定义,做一件事情:如果开启了 CONFIG_PRINTK,就通过 printk 输出内核信息。

486         lea ignore_int,%edx

         # 这里是内核代码段,注意已经是保护模式了,所以要用代码段选择子

487         movl $(__KERNEL_CS << 16),%eax

488         movw %dx,%ax            /* selector = 0x0010 = cs */

489         movw $0x8E00,%dx        /* interrupt gate - dpl=0, present */

490

        # 载入 IDT 表的首地址

491         lea idt_table,%edi

        # 共有 256 个中断向量

492         mov $256,%ecx

493 rp_sidt:

        # 这是一个循环,用默认中断处理例程初始化 256 个中断向量

494         movl %eax,(%edi)

495         movl %edx,4(%edi)

496         addl $8,%edi

497         dec %ecx

498         jne rp_sidt

499

         # 设置几个已定义的中断向量

         # 宏定义

500 .macro  set_early_handler handler,trapno

501         lea \handler,%edx

502         movl $(__KERNEL_CS << 16),%eax

503         movw %dx,%ax

504         movw $0x8E00,%dx        /* interrupt gate - dpl=0, present */

505         lea idt_table,%edi

506         movl %eax,8*\trapno(%edi)

507         movl %edx,8*\trapno+4(%edi)

508 .endm

509              # 预先设置的中断向量

510         set_early_handler handler=early_divide_err,trapno=0               # 被零除

511         set_early_handler handler=early_illegal_opcode,trapno=6         # 操作码异常

512         set_early_handler handler=early_protection_fault,trapno=13        # 保护错误

513         set_early_handler handler=early_page_fault,trapno=14            # 缺页异常

514              # 后面一段代码定义了这四个中断向量的中断处理例程。

         # 它们都调用了 early_fault,即将当前状态、中断向量号等信息通过 early_printk printk 输出。

515         ret

  1. 检查处理器类型
    • 检查是 486 还是 386
    • get vendor info
    • 如果是 486,就 set AM, WP, NE, MP;如果是 386,就 set MP
    • save PG, PE, ET
    • check ET for 287/387
  2. 载入 GDT、IDT
    • 重新载入修改 GDT 后的段寄存器
    • DS/ES 包含着默认用户段
    • 清除 GS、LDT
  3. i386_start_kernel

如果是 SMP 架构,则由第一个 CPU 调用 start_kernel,其余 CPUs 调用 initialize_secondary

跳转到 i386_start_kernel(在 arch/x86/kernel/head32.c)

head_32.S 中的其余代码是 BSS 段、数据段。

其中,下面这段数据描述了发生未知异常时内核输出的调试信息。

655 int_msg:

656         .asciz "Unknown interrupt or fault at EIP %p %p %p\n"

657

658 fault_msg:

659 /* fault info: */

660         .ascii "BUG: Int %d: CR2 %p\n"

661 /* pusha regs: */

662         .ascii "     EDI %p  ESI %p  EBP %p  ESP %p\n"

663         .ascii "     EBX %p  EDX %p  ECX %p  EAX %p\n"

664 /* fault frame: */

665         .ascii "     err %p  EIP %p   CS %p  flg %p\n"

666         .ascii "Stack: %p %p %p %p %p %p %p %p\n"

667         .ascii "       %p %p %p %p %p %p %p %p\n"

668         .asciz "       %p %p %p %p %p %p %p %p\n"

下图为 x86 体系结构下的段描述符格式(From ULK)。

http://home.ustc.edu.cn/~boj/courses/linux_kernel/images/2_2.png

arch/x86/kernel/head32.c 中的 i386_start_kernel 只有一条语句 start_kernel(),将跳转到体系结构无关部分的 init/main.c line 534,执行核心数据结构初始化。

5 内核初始化:体系结构无关部分

5.1 核心数据结构初始化

start_kernel 为什么值得开启新的一章呢?因为我们已经跳出了体系结构相关部分,离开了复杂的汇编代码,可以在 C 语言的世界里自由翱翔了。

本节摘抄自参考文献:Linux启动过程综述

start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。

  • 输出Linux版本信息(printk(linux_banner))
  • 设置与体系结构相关的环境(setup_arch())
  • 页表结构初始化(paging_init())
  • 使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init())
  • 使用alpha_mv结构和entry.S入口初始化系统IRQ(init_IRQ())
  • 核心进程调度器初始化(包括初始化几个缺省的Bottom-half,sched_init())
  • 时间、定时器初始化(包括读取CMOS时钟、估测主频、初始化定时器中断等,time_init())
  • 提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options())
  • 控制台初始化(为输出信息而先于PCI初始化,console_init())
  • 剖析器数据结构初始化(prof_buffer和prof_len变量)
  • 核心Cache初始化(描述Cache信息的Cache,kmem_cache_init())
  • 延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay())
  • 内存初始化(设置内存上下界和页表项初始值,mem_init())
  • 创建和设置内部及通用cache("slab_cache",kmem_cache_sizes_init())
  • 创建uid taskcount SLAB cache("uid_cache",uidcache_init())
  • 创建文件cache("files_cache",filescache_init())
  • 创建目录cache("dentry_cache",dcache_init())
  • 创建与虚存相关的cache("vm_area_struct","mm_struct",vma_init())
  • 块设备读写缓冲区初始化(同时创建"buffer_head"cache用户加速访问,buffer_init())
  • 创建页cache(内存页hash表初始化,page_cache_init())
  • 创建信号队列cache("signal_queue",signals_init())
  • 初始化内存inode表(inode_init())
  • 创建内存文件描述符表("filp_cache",file_table_init())
  • 检查体系结构漏洞(对于alpha,此函数为空,check_bugs())
  • SMP机器其余CPU(除当前引导CPU)初始化(对于没有配置SMP的内核,此函数为空,smp_init())
  • 启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init())

至此,基本的核心环境已经建立起来了。

5.2 设备初始化

本节摘抄自参考文献:Linux启动过程综述

init()函数作为核心线程,首先锁定内核(仅对SMP机器有效),然后调用 do_basic_setup()完成外设及其驱动程序的加载和初始化。过程如下:

  • 总线初始化(比如pci_init())
  • 网络初始化(初始化网络数据结构,包括sk_init()、skb_init()和proto_init()三部分,在proto_init()中,将调用protocols结构中包含的所有协议的初始化过程,sock_init())
  • 创建bdflush核心线程(bdflush()过程常驻核心空间,由核心唤醒来清理被写过的内存缓冲区,当bdflush()由kernel_thread()启动后,它将自己命名为kflushd)
  • 创建kupdate核心线程(kupdate()过程常驻核心空间,由核心按时调度执行,将内存缓冲区中的信息更新到磁盘中,更新的内容包括超级块和inode表)
  • 设置并启动核心调页线程kswapd(为了防止kswapd启动时将版本信息输出到其他信息中间,核心线调用kswapd_setup()设置kswapd运行所要求的环境,然后再创建 kswapd核心线程)
  • 创建事件管理核心线程(start_context_thread()函数启动context_thread()过程,并重命名为keventd)
  • 设备初始化(包括并口parport_init()、字符设备chr_dev_init()、块设备 blk_dev_init()、SCSI设备scsi_dev_init()、网络设备net_dev_init()、磁盘初始化及分区检查等等,device_setup())
  • 执行文件格式设置(binfmt_setup())
  • 启动任何使用__initcall标识的函数(方便核心开发者添加启动函数,do_initcalls())
  • 文件系统初始化(filesystem_setup())
  • 安装root文件系统(mount_root())

至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后,init()打开/dev/console设备,重定向stdin、stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序。

init()函数到此结束,内核的引导部分也到此结束了,这个由start_kernel()创建的第一个线程已经成为一个用户模式下的进程了。此时系统中存在着六个运行实体:

  • start_kernel()本身所在的执行体,这其实是一个"手工"创建的线程,它在创建了init()线程以后就进入cpu_idle()循环了,它不会在进程(线程)列表中出现
  • init线程,由start_kernel()创建,当前处于用户态,加载了init程序
  • kflushd核心线程,由init线程创建,在核心态运行bdflush()函数
  • kupdate核心线程,由init线程创建,在核心态运行kupdate()函数
  • kswapd核心线程,由init线程创建,在核心态运行kswapd()函数
  • keventd核心线程,由init线程创建,在核心态运行context_thread()函数

参考文献

涉及的代码:

  • Linux 2.6.26 Kernel Source
  • GRUB source code
  • AWARD BIOS source code
  • 在stage2开始的活动中,我们一定要设置好C环境。GRUB下可以自由输入的指令,像kernel、boot、initrd等,都是由C来实现的。这样对保护模式的需求就呼之欲出了,最起码的,C语言通过保护模式实现了更大范围的硬件支持。 
          位于stage2中的asm.s汇编文件更多的是提供了一些基本的汇编子程序,可以称之为GRUB基本函数库。而实模式切换到保护模式的函数ENTRY(real_to_prot)以及保护模式到实模式的ENTRY(prot_to_real)就位于asm.s中。 
          如果程序通过调用ENTRY(real_to_prot)函数切入到保护模式下,那么这个子函数ENTRY(real_to_prot)是怎样具体工作的呢?首要的,是要建立合适的全局描述符表GDT,不要忘了,保护模式之所以不同于实模式首先在于寻址模式的变化。保护模式通过GDT来间接寻找内存地址。GDT标号位于asm.s的末尾,内容当然是要包含各种门描述符和段的描述符。其第一个8字节的位置并不使用。GDT的内容如下: 
    ***************************************************************** 
    gdt: 
    .word 0, 0 
    .byte 0, 0, 0, 0 ;(GDT的第一个8字节不使用) 
    .word 0xFFFF, 0 ;代码段描述符,可以看到段界限低16位为0xFFFF 
    .byte 0, 0x9A, 0xCF, 0 ;第6、7字节为1100111110011010,段界限粒度G为1,段界限高四位都为1,可寻址到0xFFFFF*4K,即4G字节;段基址为0;权限为执行/读, 
    .word 0xFFFF, 0 ;数据段描述符 
    .byte 0, 0x92, 0xCF, 0 ;权限为读/写 
    .word 0xFFFF, 0 ;16位实模式代码段描述符 
    .byte 0, 0x9E, 0, 0 ;段界限粒度G为0,段界限为0xFFFF,可寻址到0xFFFF,即64K字节;段基址为0;权限为执行/读、一致码段 
    .word 0xFFFF, 0 ;16位实模式数据段描述符 
    .byte 0, 0x92, 0, 0 ;权限为读/写 
    ***************************************************************** 
          可以看到,GDT一共包含了4个有效的段描述符(包含空描述符的话,一共是5个)。一个段描述符会指出段的32位基地址和20位段界限(即段长)。以第一个有效的段描述符为例,它的形式应该是: 
    ***************************************************************** 
    字节             二进制码 
    0            1 1 1 1 1 1 1 1 
    1           1 1 1 1 1 1 1 1 
    2           0 0 0 0 0 0 0 0 
    3           0 0 0 0 0 0 0 0 
    4           0 0 0 0 0 0 0 0 
    5           1 0 0 1 1 0 1 0 
    6           1 1 0 0 1 1 1 1 
    7           0 0 0 0 0 0 0 0 
    ***************************************************************** 
          它的含义或者它描述了一个段基址为0x00000000(32位)、段长为0xF0000乘以4k(20位长度在此处表示一个段中的页数,此刻相当于4GB)、存在于内存中、特权级为2的、可读的、系统代码段。更详细的情况可以参考段描述符的格式。其中,第6字节的D位表示缺省操作数的大小。这也是我们称后两个段描述符为伪实模式代码段或数据段的原因。 
          ENTRY(real_to_prot)函数还使用48位指针gdtdesc指向该GDT。gdtdesc是在asm.s的最后部分,内容如下: 
    **************************************************** 
    gdtdesc: 
    .word 0x27 ;GDTR界限,可以描述(0x27+1)/8=5个描述符,和上面一致 
    .long gdt ;GDTR基地址,指向gdt 
    **************************************************** 
          因为我们在ENTRY(real_to_prot)函数中使用lgdt gdtdesc指令将这个gdtdesc(GDT descriptor,GDT描述符)装入到GDTR寄存器中,这是一个48位的寄存器,用来保存GDT的32位基地址和16位GDT的界限(也即长度,除以8字节就得到描述符的数目),此处gdtdesc的内容被装载到GDTR中,含义在上面的注释中。 
          那么现在实模式到保护模式的第一步(加载全局描述符表)就完成了,接下来需要设置cr0寄存器。其中,cr0的0位是PE位(protected enable),如果置1,则保护模式启动。很明显的,ENTRY(real_to_prot)函数要这么做。通过movl %cro,%eax;orl $CR0_PE_ON,%eax; 
    movl %eax,%cr0来使cr0寄存器变为0。 
          看样子,保护模式已经ok了。然而,gemfield不得不提醒的是,实模式和保护模式中的段寄存器——虽然都为16位,但含义却是不一样的。实模式下,段寄存器装载的是段基址,而在保护模式下,却装载的是索引,对于全局描述符表GDT的索引。因此,如果不对各段寄存器从新设置的话,怎么能够想象保护模式已经诞生了呢? 
          首先cs寄存器比较特殊,它不能通过直接赋值的方法来改变值,一般是通过跳转指令来实现。所以在实模式的最后一条指令执行的时候,其实跳转指令已经被预取到队列中了。在执行完实模式的最后一条指令后,程序切换到保护模式,这条跳转指令就是执行的第一条指令。
  • 3
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
单片机bootloader是一段特定的程序代码,位于单片机的内部存储器中,用于引导其他程序的运行。它的主要作用是在单片机上电后首先运行,负责初始化硬件和软件环境,并将应用程序从外部存储器中加载到内部存储器中运行。 单片机的bootloader通常由单片机厂商提供或开发,具有以下几个功能: 1. 硬件初始化:bootloader负责初始化单片机的各个硬件模块,包括时钟、中断、IO口等,确保单片机正常工作。 2. 外部存储器访问:bootloader能够读取外部存储器,如闪存、EEPROM等,从中加载应用程序到内部存储器中,实现程序的更新或更换。 3. 应用程序验证:bootloader可以对外部存储器中的应用程序进行验证,确保其完整性和正确性,避免加载错误的程序导致单片机异常甚至损坏。 4. 应用程序跳转:当应用程序加载完成后,bootloader会将控制权转交给应用程序,让其开始正常运行。这通常通过设置程序计数器(PC)来实现。 5. 通信接口支持:bootloader通常会提供一些通信接口,如串口、CAN总线等,以便与外部设备进行通信,例如PC机进行升级或调试。 单片机bootloader的设计与应用相当重要,它能够方便地对单片机进行在线升级、故障恢复和软件调试。同时,它也要求在尽可能小的存储空间内实现各种功能,并保证安全性和稳定性。因此,bootloader的设计需要仔细考虑各种情况和要求,以满足不同应用场景下的需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值