赞
踩
一。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),主要流程为:
BIOS的主要功能概括来说包括如下几部分:
加电自检,检测 CPU 各寄存器、计时芯片、中断芯片、DMA 控制器等
枚举设备,初始化寄存器,分配中断、IO 端口、DMA 资源等
进行系统设置,存于 CMOS 中。
INT 10h、INT 13h、INT 15h 等,提供给操作系统或应用程序调用。
在POST过程结束后,将调用 INT 19h,启动自举程序,自举程序将读取引导记录,装载操作系统。
BIOS 的启动主要由 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 函数过程中实现,主要步骤如下:
自举过程即为执行中断 INT 0x19 的中断服务例程 INT19_VECT 的过程 (Bootrom.asm)
主要功能为读取引导设备第一个扇区的前 512 字节(MBR),将其读入到内存 0x0000:7C00,并跳转至此处执行。
硬盘第一个扇区的前 512 个字节是主引导扇区,由 446 字节的 MBR、64 字节的分区表和 2 字节的结束标志组成。
硬盘分区表占据主引导扇区的 64 个字节(0x01BE -- 0x01FD),可以对四个分区的信息进行描述,其中每个分区的信息占据 16 个字节。
一个分区记录有如下域:
LBA和CHS两种描述符指示相同的信息,但是指示方式有所不同:LBA (逻辑块寻址,Logical Block Addressing)指示分区的起始扇区和分区长度, 而CHS(柱面 磁头 扇区)指示首扇区和末扇区。
Linux 的启动方式包括 LILO、GRUB 等。这里结合 GRUB 源代码分析其引导过程。
GRUB 的引导过程分为 stage1、stage 1.5 和 stage 2。其中 stage1 和可能存在的 stage1.5 是为 stage2 做准备,stage2 像一个微型操作系统。
BIOS 加载 GRUB stage1(如果安装到 MBR)到 0x00007C00.
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
stage 1.5 能够读取文件系统,负责从文件系统中载入并执行 stage 2,即 GRUB 的核心映像。由于系统引导过程中不需要修改文件系统,因此只实现了文件系统的读取。
可以说,stage 1.5 是 stage 1 与 stage 2 之间的桥梁,解决了文件系统这个“先有鸡还是先有蛋”的问题。
stage2 将系统切换到保护模式,设置 C 运行环境,寻找 config 文件,执行 shell 接受用户命令,载入选定的操作系统内核。
- #ifdef STAGE1_5
- # define ABS(x) ((x) - EXT_C(main) + 0x2200)
- #else
- # define ABS(x) ((x) - EXT_C(main) + 0x8200)
- #endif
随后进入 stage2.c,执行 GRUB 的主要功能。
cmain(): 主函数,载入配置文件 menu.lst(GRUB 1)或 grub.cfg(GRUB 2),如果成功载入就进入 run_menu(),显示菜单,进入循环倒计时,如果超时就进入第一个,如果用户按了键就停止倒计时。用户作出选择后,跳转到 boot_entry(),清空屏幕、获取入口,通过 find_command 找到的函数指针调用相应的命令。
如果没有成功载入配置文件,就 enter_cmdline(),也是通过 find_command 调用相应的命令。
- struct builtin
- {
- char *name; /* 命令名称 */
- int (*func) (char *, int); /* 命令执行时调用的函数指针 */
- int flags; /* 标志,似乎未用到 */
- char *short_doc; /* 短帮助 */
- char *long_doc; /* 详细帮助 */
- };
- struct builtin *builtin_table[];
root_func (char *arg, int flags)
kernel_func (char *arg, int flags)
boot_func (char *arg, int flags)
每种文件系统都要按照 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() 方法打开文件。
根据 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)列表、一个符号列表,一个符号可以是已定义或未定义的。每个已定义的符号有地址。未定义的符号则要在链接时从其他文件中寻找其定义。
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)
- 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);
最后用断言保证链接后的目标文件不太大,且偏移量正确。
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_flags | 0x1f2 | 2 | 根目录是否只读,可用 ro 或 rw 选项指定 |
root_dev | 0x1fc | 2 | 默认的 root 设备,即 /boot 所在目录,可用 root= 选项指定 |
boot_flag | 0x1fe | 2 | 0xAA55,即主引导扇区结束标志 |
header | 0x202 | 4 | HdrS (0x53726448),内核标志 |
version | 0x206 | 2 | 启动协议版本号: major * 64 + minor |
kernel_version | 0x20e | 2 | 内核版本号 |
type_of_loader | 0x210 | 1 | Boot loader ID: Boot loader ID * 64 + Version No. |
loadflags | 0x211 | 1 | 启动选项的掩码。
|
code32_start | 0x214 | 4 | 内核解压缩前立即跳转到的 32 位 flat-mode 入口 |
ramdisk_image | 0x218 | 4 | initramfs 的 32 位线性地址 |
cmd_line_ptr | 0x228 | 4 | 内核命令行的 32 位线性地址 |
下面我们迎来了真正的起点(start_of_setup),主要流程为:
4.3 初始化与保护模式
我们终于暂时离开了汇编代码,走进 “主要” 的启动部分。这一部分在 arch/x86/boot/main.c 中。
main() 中的几个函数调用都有比较详细的注释,主要作用是初始化 boot_params,将来会经常被用到。
include/asm/bootparam.h 中定义的 boot_params 结构体 (即 zeropage) 在此完成初始化:
go_to_protected_mode() 进入保护模式,代码在 boot/pm.c。
其中 GDT_ENTRY_BOOT_CS 和 GDT_ENTRY_BOOT_DS 基地址都为零,段限长都是 4G。
下面是 GDT 数据结构示意:
下面进入 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 内核文件有以下几种:
循着 Makefile 的踪迹,我们找到了 arch/x86/boot/compressed/head_32.S,这就是大内核模式下 0x100000 开始的内存内容。
至此,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 依次完成以下动作:
lgdt pa(boot_gdt_descr)
尽管我们已经在保护模式中,但只有段机制而没有启用页机制。这里设置全局页目录与页表项,并开启分页机制。
下图示意了 Linux 的分页机制(From ULK)。
全局页目录大小为 4KB,每项大小为 4B,可以表示 4MB 的线性范围,因此页目录的大小是 __PAGE_OFFSET >> 20。
page_pde_offset = (__PAGE_OFFSET >> 20);
在实模式中,已经初始化了 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
如果是 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)。
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本身的设置。这些动作有的是公共的,有的则是需要配置的才会执行的。
至此,基本的核心环境已经建立起来了。
5.2 设备初始化
本节摘抄自参考文献:Linux启动过程综述
init()函数作为核心线程,首先锁定内核(仅对SMP机器有效),然后调用 do_basic_setup()完成外设及其驱动程序的加载和初始化。过程如下:
至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后,init()打开/dev/console设备,重定向stdin、stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序。
init()函数到此结束,内核的引导部分也到此结束了,这个由start_kernel()创建的第一个线程已经成为一个用户模式下的进程了。此时系统中存在着六个运行实体:
参考文献
涉及的代码:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。