浅析 GRUB 如何加载 linux kernel
浅析 GRUB 如何加载 linux kernel前言对于 GRUB 的加载流程,网上绝大部分都是写对 menu.lst, grub.cfg 这些 GRUB 配置文件的编写流程,就像是写脚本语言一样,用些关键字就能让 PC机能正确启动桌面 Linux 了。但这只是 GRUB 的使用,而不是GRUB的分析。
本来是没有想要探究 GRUB 的想法,直到我在自制toy kernel 的学习中进入了 “虚拟内存管理”这一章节。很多介绍虚拟内存管理的时候都会说到 linux 的内存管理,linux 内核会加载到系统 3G~4G 的虚拟内存中, 但 GRUB 是没有开启虚拟内存的,linux 内核的加载是被谁,又是如何加载相应段到 3G~4G 区的呢。
分析 kernelvmlinux我们看下内核源码编译后的最原始文件 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搜索后可以看到 vmlinuz 是可引导的,压缩的内核。 initrd 是"initial ramdisk" 的简写,是临时的虚拟磁盘,暂时不讨论。 因为我电脑上 vmlinuz 是64 bit的,对 64bit不太了解,所以找了个 32bit 的vmlinuz 文件来作解析。先试试readelf命令。
# readelf -S vmlinuzreadelf:错误: Unable to read in 0x7269 bytes of 节头readelf:错误: 不是 ELF 文件 - 它开头的 magic 字节错误
不是 ELF 文件, 那试试 objdump 吧。
# objdump -afh vmlinuzobjdump: vmlinuz: 不可识别的文件格式
还是不行。
这个时候之所以会相当用这些命令看 vmlinuz 文件的段信息,因为在我的 toy kernel 中使用的是 ELF 文件,而且是使用 grub 加载的,对于 ELF 文件来说内部保护若干 section, 执行时这些 section 必须要在特地的内存地址上. 使用 readelf 查看toy kernel 的 section header 信息如下.
可以看到 Addr 段就是内核运行时这些段在内存中的地址。而加载我的内核的 grub 的配置如下
title toy kernelroot (fd0)kernel /zkernelmodule /initrd
vmlinuz 之所以叫做压缩的内核,是因为它是使用 gzip 压缩后得来,而且不单单是个纯数据包,在文件开头部分内嵌有 gzip 解压缩代码,相当于"自解压"。我的内核需要由grub加载好相应的 section, 但 vmlinuz 都读不出来段如何让 grub 加载?
其实答案就在上面的 grub 配置文件里,在 linux 中声明内核使用的是 linux 关键字,在我的配置中声明内核使用的却是 kernel. 可以明确看出,grub对 linux 的加载是特殊对待的, 但具体怎么特使对待,只能从源代码里看了。
分析 GRUB 源码从GRUB 官网上下载的是 GRUB2 的代码,结构更清晰。因为有关键字 "linux", 我们就用 "linux" 来搜索,可以搜到如下代码.
grub-core/loader/i386/pc/linux.ccmd_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 如何自己配置虚拟内存等就要留到以后再说了。