对于 GRUB 的加载流程,网上绝大部分都是写对 menu.lst, grub.cfg 这些 GRUB 配置文件的编写流程,就像是写脚本语言一样,用些关键字就能让 PC机能正确启动桌面 Linux 了。但这只是 GRUB 的使用,而不是GRUB的分析。
本来是没有想要探究 GRUB 的想法,直到我在自制toy kernel 的学习中进入了 “虚拟内存管理”这一章节。很多介绍虚拟内存管理的时候都会说到 linux 的内存管理,linux 内核会加载到系统 3G~4G 的虚拟内存中, 但 GRUB 是没有开启虚拟内存的,linux 内核的加载是被谁,又是如何加载相应段到 3G~4G 区的呢。
我们看下内核源码编译后的最原始文件 vmlinux。该文件是 ELF 文件,使用 readelf 读下该文件的 Section header.
这里只截了几个段显示,后面的段都类似. 可以看到这些需要加载的段的地址的确是在 0xC0000000 之后。但 vmlinux 并不是可引导的linux 内核文件。
linux启动的相关信息一般都在 /boot
下,我们看下里面的内容.
可以看到 grub 文件夹,grub 就是引导 linux 进行启动的 bootloader,我们看下 /boot/grub/grub.cfg
文件的内容.
menuentry 'Linux Mint 17 {
recordfail
gfxmode $linux_gfx_mode
insmod gzio
insmod part_msdos
insmod ext2
set root='hd0,msdos1'
linux /boot/vmlinuz-3.13.0-24-generic
initrd /boot/initrd.img-3.13.0-24-generic
}
带有 linux 的一行就指定了启动的内核,可以看到不是 vmlinux 文件,而是 vmlinuz 文件。
搜索后可以看到 vmlinuz 是可引导的,压缩的内核。 initrd 是"initial ramdisk" 的简写,是临时的虚拟磁盘,暂时不讨论。 因为我电脑上 vmlinuz 是64 bit的,对 64bit不太了解,所以找了个 32bit 的vmlinuz 文件来作解析。先试试readelf
命令。
# readelf -S vmlinuz
readelf:错误: Unable to read in 0x7269 bytes of 节头
readelf:错误: 不是 ELF 文件 - 它开头的 magic 字节错误
不是 ELF 文件, 那试试 objdump
吧。
# objdump -afh vmlinuz
objdump: vmlinuz: 不可识别的文件格式
还是不行。
这个时候之所以会相当用这些命令看 vmlinuz 文件的段信息,因为在我的 toy kernel 中使用的是 ELF 文件,而且是使用 grub 加载的,对于 ELF 文件来说内部保护若干 section, 执行时这些 section 必须要在特地的内存地址上. 使用 readelf
查看toy kernel 的 section header 信息如下.
可以看到 Addr 段就是内核运行时这些段在内存中的地址。而加载我的内核的 grub 的配置如下
title toy kernel
root (fd0)
kernel /zkernel
module /initrd
vmlinuz 之所以叫做压缩的内核,是因为它是使用 gzip 压缩后得来,而且不单单是个纯数据包,在文件开头部分内嵌有 gzip 解压缩代码,相当于"自解压"。我的内核需要由grub加载好相应的 section, 但 vmlinuz 都读不出来段如何让 grub 加载?
其实答案就在上面的 grub 配置文件里,在 linux 中声明内核使用的是 linux 关键字,在我的配置中声明内核使用的却是 kernel. 可以明确看出,grub对 linux 的加载是特殊对待的, 但具体怎么特使对待,只能从源代码里看了。
从GRUB 官网上下载的是 GRUB2 的代码,结构更清晰。因为有关键字 "linux", 我们就用 "linux" 来搜索,可以搜到如下代码.
grub-core/loader/i386/pc/linux.c
cmd_linux = grub_register_command("linux", grub_cmd_linux, 0, N_("Load Linux."));
从名称应该能看出就是这个, 注册了一个命令,关键字是 “linux”。进入 grub_cmd_linux 函数进行分析。
struct linux_kernel_header lh;
file = grub_file_open (argv[0]);//打开 vmlinuz
//从 vmlinuz 开头处读取 linux header 结构
grub_file_read (file, &lh, sizeof (lh)) != sizeof (lh);
//校验合法性
if(lh.boot_flag != grub_cpu_to_le16 (0xaa55));
goto fail;
//拷贝 linux header 结构体到特地地址
grub_memmove (grub_linux_real_chunk, &lh, sizeof (lh));
//拷贝 vmlinux 的数据到特地地址
grub_file_read (file, grub_linux_prot_chunk, grub_linux16_prot_size)
大部分和讨论无关的代码都被省略了,只保护加载相关代码。可以看到 vmlinuz 在文件头部提供了一个结构体给 bootloader使用来判断相关信息,使用 hexedit 打开vmlinuz 也确实可以看到这些数据,而且该结构体是双向的,即在vmlinuz 中提供给 bootloader 相关信息, bootloader 也会将内核需要的信息填到其中。可以在THE LINUX/x86 BOOT PROTOCOL中看到该结构体更多信息。
grub 并没有更详细的分析 vmlinuz 相关信息,加载后就会跳到内核部分进行下一步动作。详细分析可以看下该链接.
再回头看下 grub-core/loader/multiboot_elfxx.c ,这个应该就是加载 ELF 内核时执行的动作了。
static grub_err_t CONCAT(grub_multiboot_load_elf, XX) (grub_file_t fileconst char *filename, void *buffer)
{
Elf_Ehdr *ehdr = (Elf_Ehdr *) buffer;
char *phdr_base;
int i;
if (ehdr->e_ident[EI_MAG0] != ELFMAG0
|| ehdr->e_ident[EI_MAG1] != ELFMAG1
|| ehdr->e_ident[EI_MAG2] != ELFMAG2
|| ehdr->e_ident[EI_MAG3] != ELFMAG3
|| ehdr->e_ident[EI_DATA] != ELFDATA2LSB)
return grub_error(GRUB_ERR_UNKNOWN_OS, N_("invalid arch-independent ELF magic"));
/* Load every loadable segment in memory. */
for (i = 0; i < ehdr->e_phnum; i++)
{
}
可以看到 grub 对 ELF 内核的加载的确是分析了 ELF 段构成并按照 ELF 文件内的参数加载了。至于之后 kernel 如何自己配置虚拟内存等就要留到以后再说了。