grub2分析(转)

想了解操作系统bootloader过程原理的可以看过来,又勾起了一些项目回忆。。。


转:

一. 从开机到OS加载的简要流程:

   机器加电,通过bios的0x19功能调用,加载mbr(也就是boot.img)到0x7c00位置,并跳到该地址开始执行boot.img,它会读入diskboot.img到0x8000处(也是512个字节的大小),随后跳到0x8000开始执行diskboot.img,diskboot.img会通过其末尾保存的blocklist读入整个grub2 kernel到0x8200开始处,并跳转到0x8200执行grub2 kernle前端的startup.S,在该段代码中会做一些初始化,并进入保护模式,执行grub_main,在grub_main中读入整个linux内核,最后执行内核前端的setup代码,整个linux系统就启动了。(分别是怎么从磁盘上找到的?别急!)

 

二.重要汇编文件分析:

1.Boot.S分析(boot/i386/pc/boot.S)

Boot.img就是由boot.S生成的,当机器加电时,bios执行0x19调用,将mbr加载到0x7c00位置,也就是从内存物理地址0x7c00开始的一个扇区的内容就是boot.img,该段代码的功能通常简单的说就是在硬盘的lba模式下,将diskboot.img读到0x7000开始的一个扇区的内存区域中,然后调用copy_buffer将刚刚读入的扇区转移到0x8000开始的地方。它读入diskboot.img的依据就是变量kernel_sector,该变量在命令grub-mkimage中被设置,它记录了diskboot.img所存放的扇区号,所以boot.img才能轻而易举的读入diskboot.img。

2.Diskboot.S分析(boot/i386/pc/diskboot.S)

    Diskboot基本上就只做了一件事,那就是把整个kernel从硬盘读入到内存物理地址0x8200开始的地方,作用有点类似于早期的bootsect.S,但显然比它要简洁、健壮。跟boot.S一样,它也有一个地址薄,不同的是boot.S只有一项,而diskboot.S记录的是一个地址的链表,称为blocklist,该链表的结点都记录了一个连续sectors的集合。该地址薄在grub-setup中填写。到此为止,整个grub2就已经从硬盘转移到了内存中。

 

3.Startup.S分析(kern/i386/pc/startup.S)

    Startup做了几件事情,虽然每一件都很简单(指代码^_^),

a.进入保护模式; b.解压kernel; c.转移所有模块,清零bss段;d.跳到grub_main函数执行。

对于压缩内核的结构下面会说明,这里只简单的提一下解压的过程,通过lzo的方式将grub2 kernel解压到0x100000(1M)的物理内存地址处,解压完毕后,又从1M的地方搬回原处(看起来就像在原地解压一样,其实中间还是有个转移的),而模块搬迁就是把整个模块放到了解压内核的末尾(防止被当作bss段被清零)。

图1.1是对刚刚描述的内容的一个简单的图示:

 

 

                         图1.1

 

三.从安装开始命令分析grub2

1../configure & make后会生成三个img文件,它们分别是 boot.img(512B),bootdisk.img(512B),kernel.img(25.6k),以及众多的.mod文件,如ext2.mod,linux.mod等,这些模块之间的依赖关系记录在moddep.lst文件中.(如果中途报错找不到lzo library,可在网上下载lzo-1.08.tar,安装了lzo后再编译grub2)

 

2.grub-mkimage -v -d . -o core.img  _chainext2,该命令中 -d . 表示命令中需要的用到模块和img文件均在当前目录下去找, -o core.img 指明输出的目标文件,从_chain开始,就是可选的模块部分,命令中的模块以及与它们所依赖的模块(在moddep.lst说明)将都会被写到core.img中去。Core.img中的内容概括起来可以分为3个部分,前面是diskboot.img,然后是kernel.img的开头1k,最后是(kernel.img的后面部分+模块)的压缩体。该命令会填充boot.img的kernel_sector变量,以及startup.S中的grub_total_module_size grub_kernel_image_size等变量。

 

3.grub-setup-b boot.img -c core.img -r "(hd0,0)" "(hd0)"

根据命令参数设置好root_devdest_dev prefix dev_map等变量,例如上面的命令中root_dev=”hd0,0” dest_dev=”hd0” 等等,并根据dev_map(/boot/grub/device.map)来初始化map[]数组,如/boot/grub/device.map中有一行为”(hd0)  /dev/hda”,那么map[0x80]=“/dev/hda”,0x80是drive号。

 

注册两个文件系统fat 和 ext2,因为以后要在磁盘上读写数据,跟文件系统有关(但是为什么只有fat 和ext2 ?);注册一个biosdisk设备grub_util_biosdisk_dev,其实就是注册一个有关读写等操作的封装结构体,由于此时linux系统是在运行着的,而linux下把设备都看成是文件,所以对磁盘的读写也可以当成是对文件的读写(如/dev/hda)。注意它与grub-main中注册的grub_biosdisk_dev不同,后者是在linux系统还没起来时提供对于磁盘读写的函数,这些函数底层都是bios功能调用,因为这个时候显然不能调用还没“出生“的read、write等。^_^

 

进入复杂的setup函数,setup所做的事情简单的说就是在boot.img 、core.img相应位置上填写的一些地址和配置信息(跟命令参数有关)。,然后把boot.img和core.img再写回到磁盘上。

其中boot.img中填写boot_device和kernel_sector(最好参阅boot.S源代码),对于前者,如果boot_device 和dest_device相同,那么填0xff,否则就填写boot_device的disk id; 后者存放core.img的存放在磁盘上的第一个扇区的位置(扇区号),这样做才能找到diskboot.img。而且会将原来的老的mbr中的磁盘参数表以及分区表拷贝到boot.img的相应位置。前者位于bpb_start 至bpb_end之间,后者就是MBR中从偏移0x1be开始的64个字节。

在diskboot.img中填写的是block_list链表,链表中存储了grub kernel的存放扇区的位置信息,顺着该链表就可以读入整个grub kernel了。

其次startup.S(kernel.img的前端部分)中的dos_part,bsd_part等变量也在这里填充。

 

 

 

 

 

 

 

四.进入grub2内核

1.grub_machine_init ();

 

grub_console_init ()

注册一个全局的结构体变量,该结构体中定义了一些对终端进行操作的函数,包括设置光标、输出字符等,其底层均是调用相应的bios功能调用(主要是0x10),有了这些函数才能显示grub的操作界面,以及获取输入的命令。

 

 

接下来是有关内存方面的设置,将所有连续的内存(1M以上)都记录在mem_regions数组中,并对每一段内存进行初始化(在每一段前头加上头部信息),将它们都链接到一个链表base中,设置os的所使用内存为1M以后的那一段,大小为该段的3/4(为什么不用全部,是因为这样会更加安全)。

 

grub_biosdisk_init ()

注册一个全局的结构体变量grub_biosdisk_dev,其中封装了有关磁盘读写的函数,其底层主要是bios 0x13功能调用,功能主要是读入n个扇区以及写n个扇区。

 

设置环境变量prefix=“(hd0,0)/boot/grub“,并将该环境变量填到grub_env环境变量数组(下标的得到采用的hash算法)以及grub_env_sorted链表中。

 

图1.2是概该部分的内存示意图:

 

 

                     图1.2

 

 

 

2.显示"Welcome to GRUB!\n\n"

 

3.grub_set_root_dev ();

   根据前面注册的环境变量prefix,设置全局变量grub_device_root =“hd0,0“

 

 

 

 

4.grub_load_modules ()

对于从地址grub_end_addr到地址(grub_end_addr +grub_total_module_size)之间的每个模块,调用grub_dl_load_core函数,将模块加载,该函数的第一个参数为.mod文件的内存存放地址,第二个参数为文件的长度。(在这部分开始之间,建议大家先学习下elf文件的格式,后面附录有一部分。)

      在grub_dl_load_core函数中,会依次调用以下函数:

 

grub_arch_dl_check_header()

检查该.mod文件是否是合法的elf格式。

 

      grub_dl_resolve_name ()

           搜索文件中的.modnamesection,得到模块名字,填充mod->name区域。

 

      grub_dl_resolve_dependencies()

           搜索文件中的.moddepssection ,将该模块所有所依赖模块的信息填充mod->dep域。

 

      grub_dl_load_segments ()

           对所有SHF_ALLOCsection(也就是在运行是要占据内存的section),填充mod->segment。

 

grub_dl_resolve_symbols()

           遍历整个symbol table,将sym->st_value设置为绝对地址,其中mod->init,mod->fini执行代码地址也是在这里设置。

 

      grub_dl_call_init ()

          调用mod->init初始化函数,其实就是注册一个命令以及与该命令对应的操作。

 

      grub_dl_add()

          将该模块加入grub_dl_add模块队列中。

 

5.grub_add_unused_region ()

释放从内存地址grub_end_addr到(grub_end_addr + grub_total_module_size)之间的整个内存区域。因为上面对各个mod section的属性做了记录,因此该部分也就不需要了(相当于得到可执行文件以后删除源代码^_^)。

 

6.grub_load_normal_mode ()

       加载normal模块,该模块是许多模块(如ls,cat等)的依赖模块,所以大部分情况下该模块已经在上面的grub_load_modules中已经加载了。

7.grub_enter_rescue_mode ()

依次调用以下函数:

  attempt_normal_mode ();

在终端上显示grub>,并注册normal模式下的以下几个命令,normal,title,rescue,set,unset,lsmod,insmod,rmmod。并把这些命令都链到grub_rescue_command_list队列中。如输入rescue后回车就可以进入到grub rescue>模式。

 

注册rescue模式下的以下几个命令,normal,title,rescue,set,unset,lsmod,insmod,rmmod。把这些命令都链到grub_rescue_command_list队列中。

打印grub rescue>提示符,获得用户输入,然后执行对应于该命令的注册函数。

对于linux的启动来说,依次输入命令

grubrescue>linux (hd0)/boot/vmlinuz

grubrescue>boot

就可以启动linux系统了(?),详细的启动过程见图1.3:

 

图中的第一步和第二步在linux命令的注册函数grub_rescue_cmd_linux中完成,第三步和第四步在startup.S的grub_linux_boot_bzimage函数中完成,(只分析了大内核的情况)。

 

 

图1.3

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值