红联Linux门户
Linux帮助

浅析GRUB如何加载Linux kernel

发布时间:2015-02-20 10:52:29来源:linux网站作者:ziyang3721

前言

对于 GRUB 的加载流程,网上绝大部分都是写对 menu.lst, grub.cfg 这些 GRUB 配置文件的编写流程,就像是写脚本语言一样,用些关键字就能让 PC机能正确启动桌面 Linux 了。但这只是 GRUB 的使用,而不是GRUB的分析。

本来是没有想要探究 GRUB 的想法,直到我在自制toy kernel 的学习中进入了 “虚拟内存管理”这一章节。很多介绍虚拟内存管理的时候都会说到 Linux 的内存管理,Linux 内核会加载到系统 3G~4G 的虚拟内存中, 但 GRUB 是没有开启虚拟内存的,Linux 内核的加载是被谁,又是如何加载相应段到 3G~4G 区的呢。


分析 kernel

vmLinux

我们看下内核源码编译后的最原始文件 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 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 源码
从[GRUB 官网](http://www.gnu.org/software/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 https://www.kernel.org/doc/Documentation/x86/boot.txt  中看到该结构体更多信息。

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 文件内的参数加载了。