Grub2 · 2023年3月28日 0

Linux Grub2原理学习

Grub2 原理学习及源码分析。

原理相关

Boot Loaders 和 Boot Managers 的区别

  • Boot managers:为一个提供启动选项的菜单,或以其他方式来提供控制启动过程的程序。用户可以选择一个选项,然后boot manager将控制权交给选择的工具。
  • Boot loaders:负责将操作系统内核加载到内存中,并启动内核运行。一般还会有其他支持文件,如Linux的initial RAM disk文件(initrd)

然而Grub(包括grub legacy和grub2),它既是boot manager又是boot loader。

而有些程序只能加载linux内核的,就只是boot loader了(如ELILO、SYSLINUX、EFI stub loader)

有些程序则只是boot manager(如rEFIt、rEFInd、gummiboot/systemd-boot)。

linux 启动分区结构

用dd命令导出efi分区数据,可以直接解压看到里面的文件:

image-20230328112437114
image-20230328112638013

从EFI启动原理上讲,EFI启动过程依赖于ESP上的EFI Boot loader和BIOS中设置的NVRAM变量中指向这些boot loader的指针。然而有些EFI的bug或者NVRAM变量损坏会导致无法正常引导(如拔掉硬盘等操作都会导致NVRAM变量消失),需要重新创建相应的变量才能恢复启动。

在windows下面很简单,很多uefi bios都被设计成会默认寻找windows boot loader,但Linux发行版如果找不到对应的引导项就直接停止启动了。

为了缓解这个问题,linux开发者创建了fallback.efi程序,现在很少用这个名字,通常是将shim或shim-signed,并常作为fbx64.efi文件出现在linux文件系统中。

无论是哪种文件名,fallback.efi都是一种boot manager(而不是加载启动选项菜单),它会遍历/EFI子目录,找到BOOT.CSV/BOOTX64.CSV文件,然后读取并处理它,来生成一个新的NVRAM变量。CSV文件为由逗号分隔的数据元素,如下例:

shimx64.efi,ubuntu,,This is the boot entry for ubuntu
  • filename(shimx64.efi) —— 要添加到NVRAM启动管理表中的文件名,和BOOT.CSV在同一个目录下。
  • label(ubuntu) —— 与文件相关联的标签,由固件内置的boot manager显示。
  • options —— 指定Boot loader需要的选项,大多数时候为空。
  • description —— 描述。

下面是原始ubuntu的EFI系统分区结构:

EFI/
  BOOT/
      BOOTX64.EFI   #通用EFI启动程序,用于启动UEFI兼容的操作系统
      fbx64.efi   #UEFI固件的帧缓冲驱动程序,用于提供图形化的启动界面
      mmx64.efi   #UEFI固件的内存映射IO驱动程序,用于提供对系统内存的访问
  ubuntu/
      BOOTX64.CSV   #NVRAM变量配置
      grub.cfg   #grub配置文件
      grubx64.efi   #grub引导主程序,用于启动Ubuntu操作系统
      mmx64.efi
      shimx64.efi   #用于安全启动的引导程序,用于验证GRUB引导程序的数字签名

grub 2原理

命令行输入下面命令可查看当前系统是否安装grub2:

grub-install -V
image-20230328160320796

ubuntu设置开机进入grub2方法:

sudo gedit /etc/default/grub

修改文件内容,注释掉GRUB_TIMEOUT_STYLE=hidden这行,改为menu,并加上TIMEOUT时间:

GRUB_DEFAULT=0
GRUB_TIMEOUT_STYLE=menu
#GRUB_TIMEOUT_STYLE=hidden
GRUB_TIMEOUT=10

然后更新配置:

sudo update-grub

重启就能自动进入grub了:

image-20230328173554336

grub 2引导操作系统方式

grub 2支持两种引导方式:

  • 直接引导(direct-load):直接通过默认的grub2 boot loader来引导写在默认配置文件中的操作系统。
  • 链式引导(chain-load):使用grub2 boot loader链式引导另一个boot loader,该Boot loader将引导对应的操作系统。

grub2 配置

grub2的默认配置文件为/boot/grub/grub.cfg

在此之前,ESP分区中有/efi/ubuntu/grub.cfg文件,将配置文件重定向到/boot/grub/grub.cfg

search.fs_uuid 36f455bf-5513-4008-9790-8ff934ed60ae root hd0,gpt2
set prefix=($root)'/boot/grub'
configfile $prefix/grub.cfg

/boot/grub/grub.cfg通常是通过grub2-mkconfig程序来生成的,默认情况下它会自动探测操作系统内核,并生成对应的操作系统菜单项,使用命令:

grub2-mkconfig -o /boot/grub/grub.cfg

grub2-mkconfig是根据/etc/default/grub文件来创建配置文件的。如上面讲的,可以修改该文件来使开机进入grub菜单,具体一些宏定义这里就不细讲了。

下面是/boot/grub/grub.cfg配置文件中用于定义grub引导菜单条目信息的命令:

# 设置 linux_gfx_mode 变量为 keep,用于保持当前图形模式
export linux_gfx_mode
# 定义 Ubuntu 操作系统的 GRUB 引导菜单条目
menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-36f455bf-5513-4008-9790-8ff934ed60ae' {
# 如果上一次启动失败,则记录失败信息
recordfail
# 加载视频驱动
load_video
# 设置图形模式为 linux_gfx_mode 变量的值
gfxmode $linux_gfx_mode
# 加载 gzip 模块
insmod gzio
# 如果 grub_platform 变量的值为 xen,则加载 xzio 和 lzopio 模块
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
# 加载 GPT 分区模块和 ext2 文件系统模块,并设置根分区
insmod part_gpt
insmod ext2
set root='hd0,gpt2'
# 根据 UUID 查找根分区
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt2 --hint-efi=hd0,gpt2 --hint-baremetal=ahci0,gpt2 36f455bf-5513-4008-9790-8ff934ed60ae
else
search --no-floppy --fs-uuid --set=root 36f455bf-5513-4008-9790-8ff934ed60ae
fi
# 设置 Linux 内核启动参数,包括根分区 UUID、只读模式、安静模式和启动画面
linux /boot/vmlinuz-5.15.0-69-generic root=UUID=36f455bf-5513-4008-9790-8ff934ed60ae ro quiet splash $vt_handoff
# 加载 initrd 镜像
initrd /boot/initrd.img-5.15.0-69-generic
}

img文件

grub由几个img镜像组成,各种用于不同方式启动的grub引导镜像(boot.img)、一个内核镜像(kernel.img)和一些与内核镜像结合起来形成核心镜像(core.img)的模块。下图为各个img文件之间的关系:

image-20230331101046396

在传统BIOS+MBR启动模式下:

  • boot.img为grub启动的第一个img文件,是被写入到MBR中长度为512字节的数据。它负责读取属于core.img的第一个扇区,并跳转到该位置,将控制权交给core.img的入口点。一般core.img的地址是硬编码到Boot.img中的(*感觉可以利用这个来劫持)。
  • core.img中包含了多个img文件的内容,是根据diskboot.img、kernel.img和一系列模块来动态创建的。包含的功能模块在支撑grub能正常工作,如配置文件解析器、文件系统解析器、内存管理器、多重引导器等。

当BIOS将控制权转移到MBR中的boot loader后,boot.img被加载到内存中,执行核心代码,跳转到core.img的入口点,core.img读取grub.cfg,生成菜单界面,等待用户选择加载项,选择后会加载对应操作系统内核和initrd镜像,并将控制权转移到内核,启动操作系统。

启动流程

  1. BIOS自检:计算机开机时,首先由BIOS(Basic Input/Output System)进行自检,检查硬件是否正常。
  1. 引导加载程序(bootloader):BIOS自检完成后,会从硬盘、光盘或USB设备等存储介质中读取引导加载程序。Ubuntu使用Grub2作为默认的引导加载程序,它会读取/boot/grub/grub.cfg配置文件并显示菜单,用户可以选择要启动的操作系统或内核。
  1. 内核加载:引导加载程序会加载Ubuntu内核文件(vmlinuz),并将其解压缩到内存中。

由于Grub2即作为启动管理器又作为boot loader,grubx64.efi会根据配置信息来加载操作系统内核和模块。如下面的配置命令:linux /boot/vmlinuz-5.15.0-69-generic root=UUID=36f455bf-5513-4008-9790-8ff934ed60ae ro quiet splash $vt_handoff

  1. 初始化进程(init):内核加载完成后,会启动初始化进程(init)。init进程会读取/etc/inittab配置文件并启动其他系统进程。
  1. 运行级别(runlevel):Ubuntu系统有7个运行级别,每个运行级别都对应着不同的系统状态。init进程会根据配置文件中指定的运行级别启动相应的服务和进程。
  1. 系统服务启动:在特定的运行级别下,init进程会启动相应的系统服务,如网络服务、文件系统服务等。
  1. 登录管理器(display manager):在图形界面下,登录管理器会启动并显示登录界面,等待用户输入用户名和密码。

initrd镜像

initrd(Initial RAM Disk)是一个临时文件系统,它被用于在Linux系统启动时提供必要的文件和驱动程序。initrd镜像包含了操作系统启动所需的所有文件和驱动程序,包括根文件系统、设备驱动程序、网络协议栈、文件系统工具等等。当Linux系统启动时,initrd镜像会被加载到内存中,然后被挂载为根文件系统,从而提供了一个最小化的运行环境,使得系统能够启动并加载必要的驱动程序,最终完成系统初始化。

cpio压缩格式的initrd.img文件处理流程:

1.boot loader把内核以及initrd文件加载到内存的特定位置。

2.内核判断initrd的文件格式,如果是cpio格式。

3.将initrd的内容释放到rootfs中。

4.执行initrd中的/init文件,执行到这一点,内核的工作全部结束,完全交给/init文件处理。

dmesg查看启动日志,可以看到部分日志,但不全:

image-20230410110249027

重启进入Grub,选中启动项,按e进入编辑模式,删除linux这行的quiet选项,按ctrl+x启动系统:

image-20230410154010522

命令行输入下面命令查看grub日志:sudo cat /var/log/boot.log

EFI分区文件调用流程

在没有安全启动的系统下,EFI分区文件调用流程是:

  1. 读取BOOT.CSV/BOOTX64.CSV文件,处理NVRAM启动项。
  2. 由于NVRAM设置shimx64.efi为入口点,系统会先加载ubuntu/shimx64.efi,其中init_grub函数负责加载fbx64.efi、mmx64.efi模块,提供图形显示和内存管理的支持,然后再加载并启动grubx64.efi。加载这些模块的过程是通过调用UEFI固件提供的LoadImageStartImage函数实现的。LoadImage函数用于加载模块文件,而StartImage函数用于启动模块文件中的代码。image-20230412154907392
  3. grubx64.efi读取grub.cfg,处理引导配置信息,等用户选择指定的启动项后或使用默认启动项,加载操作系统内核和initrd,并将控制权转交给内核。

grub2源码编译与分析

依赖:

* GCC 5.1.0 or later
Experimental support for clang 8.0.0 or later (results in much bigger binaries)
for i386, x86_64, arm (including thumb), arm64, mips(el), powerpc, sparc64
* GNU Make
* GNU Bison 2.3 or later
* GNU gettext 0.17 or later
* GNU binutils 2.9.1.0.23 or later
* Flex 2.5.35 or later
* pkg-config
* GNU patch
* Other standard GNU/Unix tools
* a libc with large file support (e.g. glibc 2.1 or later)

On GNU/Linux, you also need:

* libdevmapper 1.02.34 or later (recommended)


* Python 3 (NOTE: python 2.6 should still work, but it's not tested)
* Autoconf 2.64 or later
* Automake 1.14 or later

安装好依赖后,运行./bootstrap脚本,生成配置。

配置编译选项,设置为调试模式,其中–prefix选项指定安装目录,CFLAGS指定编译调试选项,目标平台为x86_64-efi平台:

./configure --target=x86_64 --with-platform=efi --prefix=/home/edvison/grub2 CFLAGS="-g"

安装:

make
make install

生成efi:

MODULES="search iso9660 configfile normal memdisk tar part_msdos part_gpt fat"<br>./grub-mkimage -p . -d ./grub-core/ -O x86_64-efi -o grubx64.efi $MODULES

-d选项指定使用./grub-core目录下的image和module,-O指定生成镜像的格式,-o为输出文件,后面可接复数包含在./grub-core目录下的模块名。

grub-mkimage分析

看到前面grubx64.efi是通过grub-mkimage来生成的,但其中包含哪些镜像和模块仍不太清楚,还是得从源码来进行分析。

grub-mkimage入口函数定义在util/grub-mkimage.c中,该文件主要负责命令行参数初始化和解析,就不细看了:

image-20230413152438809

重点关注image生成部分,源码在util/mkimage.c中。

grub_install_image_target_desc结构体定义了各个格式的镜像成员的元素,下面是x86_64-efi的image_target定义:

static const struct grub_install_image_target_desc image_targets[] =
{
...
{
.dirname = "x86_64-efi",
.names = { "x86_64-efi", NULL },
.voidp_sizeof = 8,
.bigendian = 0,
.id = IMAGE_EFI,
.flags = PLATFORM_FLAGS_NONE,
.total_module_size = TARGET_NO_FIELD,
.decompressor_compressed_size = TARGET_NO_FIELD,
.decompressor_uncompressed_size = TARGET_NO_FIELD,
.decompressor_uncompressed_addr = TARGET_NO_FIELD,
.section_align = GRUB_PE32_SECTION_ALIGNMENT,
.vaddr_offset = EFI64_HEADER_SIZE,
.pe_target = GRUB_PE32_MACHINE_X86_64,
.elf_target = EM_X86_64,
},
...
};

grub_install_generate_image负责生成指定的镜像:

void
grub_install_generate_image (const char *dir, const char *prefix,
FILE *out, const char *outname, char *mods[],
char *memdisk_path, char **pubkey_paths,
size_t npubkeys, char *config_path,
const struct grub_install_image_target_desc *image_target,
int note, grub_compression_t comp, const char *dtb_path,
const char *sbat_path, int disable_shim_lock)
{
char *kernel_img, *core_img;
size_t total_module_size, core_size;
size_t memdisk_size = 0, config_size = 0;
size_t prefix_size = 0, dtb_size = 0, sbat_size = 0;
char *kernel_path;
size_t offset;
struct grub_util_path_list *path_list, *p;
size_t decompress_size = 0;
struct grub_mkimage_layout layout;

if (comp == GRUB_COMPRESSION_AUTO)
comp = image_target->default_compression;

if (image_target->id == IMAGE_I386_PC
|| image_target->id == IMAGE_I386_PC_PXE
|| image_target->id == IMAGE_I386_PC_ELTORITO)
comp = GRUB_COMPRESSION_LZMA;

path_list = grub_util_resolve_dependencies (dir, "moddep.lst", mods);

kernel_path = grub_util_get_path (dir, "kernel.img");

if (image_target->voidp_sizeof == 8)
total_module_size = sizeof (struct grub_module_info64);
else
total_module_size = sizeof (struct grub_module_info32);

{
size_t i;
for (i = 0; i < npubkeys; i++)
{
size_t curs;
curs = ALIGN_ADDR (grub_util_get_image_size (pubkey_paths[i]));
grub_util_info ("the size of public key %u is 0x%"
GRUB_HOST_PRIxLONG_LONG,
(unsigned) i, (unsigned long long) curs);
total_module_size += curs + sizeof (struct grub_module_header);
}
}

comp参数指定压缩方式,默认为GRUB_COMPRESSION_AUTO,即LZMA压缩。

path_list保存命令行参数传入的模块,同时保存moddep.lst文件中每个模块依赖的所有模块。

kernel_path保存kernel.img的路径。

grub_util_get_image_size计算每个模块的大小,并统计到total_module_size中。//检测voidp_sizeof的值,判断64位or32位,选择对应的函数加载内核镜像。

if (image_target->voidp_sizeof == 4)
kernel_img = grub_mkimage_load_image32 (kernel_path, total_module_size,
&layout, image_target);
else
kernel_img = grub_mkimage_load_image64 (kernel_path, total_module_size,
&layout, image_target);
// Xen or Xen PVH平台,且内存对齐小于4096,设置对齐为4096
if ((image_target->id == IMAGE_XEN || image_target->id == IMAGE_XEN_PVH) &&
layout.align < 4096)
layout.align = 4096;
// 目标平台支持解压缩器,总模块大小不为TARGET_NO_FIELD,在内核镜像尾部添加模块大小
if ((image_target->flags & PLATFORM_FLAGS_DECOMPRESSORS)
&& (image_target->total_module_size != TARGET_NO_FIELD))
*((grub_uint32_t *) (kernel_img + image_target->total_module_size))
= grub_host_to_target32 (total_module_size);
// 如平台要求模块在内核之前加载,则将镜像中的模块数据移到内核后,并清空之前模块的内存
if (image_target->flags & PLATFORM_FLAGS_MODULES_BEFORE_KERNEL)
{
memmove (kernel_img + total_module_size, kernel_img, layout.kernel_size);
memset (kernel_img, 0, total_module_size);
}

后面几段基本都是处理模块和内核镜像的代码。之后压缩内核镜像和模块,将其存储到core_img中:

grub_util_info ("kernel_img=%p, kernel_size=0x%" GRUB_HOST_PRIxLONG_LONG,
kernel_img,
(unsigned long long) layout.kernel_size);
compress_kernel (image_target, kernel_img, layout.kernel_size + total_module_size,
&core_img, &core_size, comp);
free (kernel_img);

grub_util_info ("the core size is 0x%" GRUB_HOST_PRIxLONG_LONG,
(unsigned long long) core_size);

if (!(image_target->flags & PLATFORM_FLAGS_DECOMPRESSORS)
&& image_target->total_module_size != TARGET_NO_FIELD)
*((grub_uint32_t *) (core_img + image_target->total_module_size))
= grub_host_to_target32 (total_module_size);

之后就是检查image_target->id,根据不同平台来处理镜像的代码了,平台很多,直接看IMAGE_EFI格式:

case IMAGE_EFI:
{
char *pe_img, *pe_sbat, *header;
struct grub_pe32_section_table *section;
size_t n_sections = 4;
size_t scn_size;
grub_uint32_t vma, raw_data;
size_t pe_size, header_size;
struct grub_pe32_coff_header *c;
static const grub_uint8_t stub[] = GRUB_PE32_MSDOS_STUB;
struct grub_pe32_optional_header *o32 = NULL;
struct grub_pe64_optional_header *o64 = NULL;

if (image_target->voidp_sizeof == 4)
header_size = EFI32_HEADER_SIZE;
else
header_size = EFI64_HEADER_SIZE;

vma = raw_data = header_size;

if (sbat_path != NULL)
{
sbat_size = ALIGN_ADDR (grub_util_get_image_size (sbat_path));
sbat_size = ALIGN_UP (sbat_size, GRUB_PE32_FILE_ALIGNMENT);
}

pe_size = ALIGN_UP (header_size + core_size, GRUB_PE32_FILE_ALIGNMENT) +
ALIGN_UP (layout.reloc_size, GRUB_PE32_FILE_ALIGNMENT) + sbat_size;
header = pe_img = xcalloc (1, pe_size);

memcpy (pe_img + raw_data, core_img, core_size);

/* The magic. */
memcpy (header, stub, GRUB_PE32_MSDOS_STUB_SIZE);
memcpy (header + GRUB_PE32_MSDOS_STUB_SIZE, "PE\0\0",
GRUB_PE32_SIGNATURE_SIZE);

/* The COFF file header. */
c = (struct grub_pe32_coff_header *) (header + GRUB_PE32_MSDOS_STUB_SIZE
+ GRUB_PE32_SIGNATURE_SIZE);
c->machine = grub_host_to_target16 (image_target->pe_target);

if (sbat_path != NULL)
n_sections++;

c->num_sections = grub_host_to_target16 (n_sections);
c->time = grub_host_to_target32 (STABLE_EMBEDDING_TIMESTAMP);
c->characteristics = grub_host_to_target16 (GRUB_PE32_EXECUTABLE_IMAGE
| GRUB_PE32_LINE_NUMS_STRIPPED
| ((image_target->voidp_sizeof == 4)
? GRUB_PE32_32BIT_MACHINE
: 0)
| GRUB_PE32_LOCAL_SYMS_STRIPPED
| GRUB_PE32_DEBUG_STRIPPED);

/* The PE Optional header. */
if (image_target->voidp_sizeof == 4)
{
c->optional_header_size = grub_host_to_target16 (sizeof (struct grub_pe32_optional_header));

o32 = (struct grub_pe32_optional_header *)
(header + GRUB_PE32_MSDOS_STUB_SIZE + GRUB_PE32_SIGNATURE_SIZE +
sizeof (struct grub_pe32_coff_header));
o32->magic = grub_host_to_target16 (GRUB_PE32_PE32_MAGIC);
o32->data_base = grub_host_to_target32 (header_size + layout.exec_size);

section = (struct grub_pe32_section_table *)(o32 + 1);
}
else
{
c->optional_header_size = grub_host_to_target16 (sizeof (struct grub_pe64_optional_header));
o64 = (struct grub_pe64_optional_header *)
(header + GRUB_PE32_MSDOS_STUB_SIZE + GRUB_PE32_SIGNATURE_SIZE +
sizeof (struct grub_pe32_coff_header));
o64->magic = grub_host_to_target16 (GRUB_PE32_PE64_MAGIC);

section = (struct grub_pe32_section_table *)(o64 + 1);
}

#if __GNUC__ >= 12
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdangling-pointer"
#endif
PE_OHDR (o32, o64, header_size) = grub_host_to_target32 (header_size);
PE_OHDR (o32, o64, entry_addr) = grub_host_to_target32 (layout.start_address);
PE_OHDR (o32, o64, image_base) = 0;
PE_OHDR (o32, o64, image_size) = grub_host_to_target32 (pe_size);
PE_OHDR (o32, o64, section_alignment) = grub_host_to_target32 (image_target->section_align);
PE_OHDR (o32, o64, file_alignment) = grub_host_to_target32 (GRUB_PE32_FILE_ALIGNMENT);
PE_OHDR (o32, o64, subsystem) = grub_host_to_target16 (GRUB_PE32_SUBSYSTEM_EFI_APPLICATION);

/* Do these really matter? */
PE_OHDR (o32, o64, stack_reserve_size) = grub_host_to_target32 (0x10000);
PE_OHDR (o32, o64, stack_commit_size) = grub_host_to_target32 (0x10000);
PE_OHDR (o32, o64, heap_reserve_size) = grub_host_to_target32 (0x10000);
PE_OHDR (o32, o64, heap_commit_size) = grub_host_to_target32 (0x10000);

PE_OHDR (o32, o64, num_data_directories) = grub_host_to_target32 (GRUB_PE32_NUM_DATA_DIRECTORIES);

/* The sections. */
PE_OHDR (o32, o64, code_base) = grub_host_to_target32 (vma);
PE_OHDR (o32, o64, code_size) = grub_host_to_target32 (layout.exec_size);
#if __GNUC__ >= 12
#pragma GCC diagnostic pop
#endif
section = init_pe_section (image_target, section, ".text",
&vma, layout.exec_size,
image_target->section_align,
&raw_data, layout.exec_size,
GRUB_PE32_SCN_CNT_CODE |
GRUB_PE32_SCN_MEM_EXECUTE |
GRUB_PE32_SCN_MEM_READ);

scn_size = ALIGN_UP (layout.kernel_size - layout.exec_size, GRUB_PE32_FILE_ALIGNMENT);
#if __GNUC__ >= 12
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdangling-pointer"
#endif
/* ALIGN_UP (sbat_size, GRUB_PE32_FILE_ALIGNMENT) is done earlier. */
PE_OHDR (o32, o64, data_size) = grub_host_to_target32 (scn_size + sbat_size +
ALIGN_UP (total_module_size,
GRUB_PE32_FILE_ALIGNMENT));
#if __GNUC__ >= 12
#pragma GCC diagnostic pop
#endif

section = init_pe_section (image_target, section, ".data",
&vma, scn_size, image_target->section_align,
&raw_data, scn_size,
GRUB_PE32_SCN_CNT_INITIALIZED_DATA |
GRUB_PE32_SCN_MEM_READ |
GRUB_PE32_SCN_MEM_WRITE);

scn_size = pe_size - layout.reloc_size - sbat_size - raw_data;
section = init_pe_section (image_target, section, "mods",
&vma, scn_size, image_target->section_align,
&raw_data, scn_size,
GRUB_PE32_SCN_CNT_INITIALIZED_DATA |
GRUB_PE32_SCN_MEM_READ |
GRUB_PE32_SCN_MEM_WRITE);

if (sbat_path != NULL)
{
pe_sbat = pe_img + raw_data;
grub_util_load_image (sbat_path, pe_sbat);

section = init_pe_section (image_target, section, ".sbat",
&vma, sbat_size,
image_target->section_align,
&raw_data, sbat_size,
GRUB_PE32_SCN_CNT_INITIALIZED_DATA |
GRUB_PE32_SCN_MEM_READ);
}

scn_size = layout.reloc_size;
#if __GNUC__ >= 12
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdangling-pointer"
#endif
PE_OHDR (o32, o64, base_relocation_table.rva) = grub_host_to_target32 (vma);
PE_OHDR (o32, o64, base_relocation_table.size) = grub_host_to_target32 (scn_size);
#if __GNUC__ >= 12
#pragma GCC diagnostic pop
#endif
memcpy (pe_img + raw_data, layout.reloc_section, scn_size);
init_pe_section (image_target, section, ".reloc",
&vma, scn_size, image_target->section_align,
&raw_data, scn_size,
GRUB_PE32_SCN_CNT_INITIALIZED_DATA |
GRUB_PE32_SCN_MEM_DISCARDABLE |
GRUB_PE32_SCN_MEM_READ);

free (core_img);
core_img = pe_img;
core_size = pe_size;
}

grubx64.efi是PE32+格式的,镜像需要对应处理为该格式,给镜像添加PE头,处理节区数据。

这里将core_img的数据复制到pe_img,进行处理,处理完毕后再拷回core_img。

由此可以看出,EFI模式下,依然是使用Kernel.img+modules的镜像数据,只是将其处理为PE32+的EFI文件了。

kernel.img分析

既然核心代码都在kernel.img中,那就得仔细分析它了。

其源代码位于/grub-core/中,先看入口点:

/grub-core/kern/main.c/* The main routine. */
void __attribute__ ((noreturn))
grub_main (void)
{
/* 初始化平台 */
grub_machine_init ();

grub_boot_time ("After machine init.");

/* This breaks flicker-free boot on EFI systems, so disable it there. */
#ifndef GRUB_MACHINE_EFI
/* Hello. */
grub_setcolorstate (GRUB_TERM_COLOR_HIGHLIGHT);
grub_printf ("Welcome to GRUB!\n\n");
grub_setcolorstate (GRUB_TERM_COLOR_STANDARD);
#endif

/* 初始化认证 API.
加载配置文件 */
grub_verifiers_init ();

grub_load_config ();

grub_boot_time ("Before loading embedded modules.");

/* 加载预启动模块并释放空间 */
grub_register_exported_symbols ();
#ifdef GRUB_LINKER_HAVE_INIT
grub_arch_dl_init_linker ();
#endif
grub_load_modules ();

grub_boot_time ("After loading embedded modules.");

/* 设置根设备和前缀,并导出相关环境变量
for convenience. */
grub_set_prefix_and_root ();
grub_env_export ("root");
grub_env_export ("prefix");

/* 回收用于模块的空间 */
reclaim_module_space ();

grub_boot_time ("After reclaiming module space.");
/* 注册核心命令,执行嵌入式配置文件 */
grub_register_core_commands ();

grub_boot_time ("Before execution of embedded config.");

if (load_config)
grub_parser_execute (load_config);

/* 进入正常启动模式,加载正常模块 */
grub_boot_time ("After execution of embedded config. Attempt to go to normal mode");

grub_load_normal_mode ();
/* 进入救援模式 */
grub_rescue_run ();
}

下面看几个主要函数。

grub_machine_init

这个函数和体系架构相关,不同平台上的实现不同。其主要负责初始化平台上的必须操作。

下面是i386 pc平台的初始化代码/kern/i386/pc/init.c:

/* Via needs additional wbinvd. */
// 为解决VIA芯片组在使用BIOS进行内存映射时的bug
static void
grub_via_workaround_init (void)
{
grub_uint32_t manufacturer[3], max_cpuid;
// 判断是否支持CPUID指令
if (! grub_cpu_is_cpuid_supported ())
return;
// 判断CPU是否为VIA芯片组
grub_cpuid (0, max_cpuid, manufacturer[0], manufacturer[2], manufacturer[1]);

if (grub_memcmp (manufacturer, "CentaurHauls", 12) != 0)
return;
// 执行wbinvd指令,清空CPU缓存
grub_bios_via_workaround1 = 0x090f;
grub_bios_via_workaround2 = 0x090f;
asm volatile ("wbinvd");
}

void
grub_machine_init (void)
{
int i;
#if 0
int grub_lower_mem;
#endif
grub_addr_t modend;

/* This has to happen before any BIOS calls. */
/* 在BIOS调用之前执行 */
grub_via_workaround_init ();

grub_modbase = GRUB_MEMORY_MACHINE_DECOMPRESSION_ADDR + (_edata - _start);

/* Initialize the console as early as possible. */
/* 初始化控制台 */
grub_console_init ();

...

grub_load_config

用于将GRUB配置文件加载到内存:

static void
grub_load_config (void)
{
struct grub_module_header *header;
// 通过 FOR_MODULES 宏遍历所有的模块,找到类型为 OBJ_TYPE_CONFIG 的模块
FOR_MODULES (header)
{
/* Not an embedded config, skip. */
if (header->type != OBJ_TYPE_CONFIG)
continue;
// 分配内存用于存储配置文件内容
load_config = grub_malloc (header->size - sizeof (struct grub_module_header) + 1);
if (!load_config)
{
grub_print_error ();
break;
}
// 将配置文件内容copy到内存中,并在末尾添加空字符
grub_memcpy (load_config, (char *) header +
sizeof (struct grub_module_header),
header->size - sizeof (struct grub_module_header));
load_config[header->size - sizeof (struct grub_module_header)] = 0;
break;
}
}

grub_load_modules

通过grub_dl_load_core()函数将类型为 OBJ_TYPE_ELF 的模块加载到内存中:

/* Load all modules in core. */
static void
grub_load_modules (void)
{
struct grub_module_header *header;
// 通过 FOR_MODULES 宏遍历所有的模块,找到类型为 OBJ_TYPE_ELF 的模块
FOR_MODULES (header)
{
/* Not an ELF module, skip. */
if (header->type != OBJ_TYPE_ELF)
continue;

if (! grub_dl_load_core ((char *) header + sizeof (struct grub_module_header),
(header->size - sizeof (struct grub_module_header))))
grub_fatal ("%s", grub_errmsg);

if (grub_errno)
grub_print_error ();
}
}

grub_dl_load_core&grub_dl_init

grub_dl_t
grub_dl_load_core (void *addr, grub_size_t size)
{
grub_dl_t mod;

grub_boot_time ("Parsing module");

mod = grub_dl_load_core_noinit (addr, size);

if (!mod)
return NULL;

grub_boot_time ("Initing module %s", mod->name);
grub_dl_init (mod);
grub_boot_time ("Module %s inited", mod->name);

return mod;
}static inline void
grub_dl_init (grub_dl_t mod)
{
if (mod->init)
(mod->init) (mod);

mod->next = grub_dl_head;
grub_dl_head = mod;
}

GRUB_MOD_INIT(linux)

/grub-core/loader/i386/linux.c

用于注册linux、initrd两个grub命令,回调函数分别为grub_cmd_linuxgrub_cmd_initrd

GRUB_MOD_INIT(linux)
{
cmd_linux = grub_register_command ("linux", grub_cmd_linux,
0, N_("Load Linux."));
cmd_initrd = grub_register_command ("initrd", grub_cmd_initrd,
0, N_("Load initrd."));
my_mod = mod;
}

grub_cmd_linux

读取linux内核镜像:

file = grub_file_open (argv[0], GRUB_FILE_TYPE_LINUX_KERNEL);
if (! file)
goto fail;

if (grub_file_read (file, &lh, sizeof (lh)) != sizeof (lh))
{
if (!grub_errno)
grub_error (GRUB_ERR_BAD_OS, N_("premature end of file %s"),
argv[0]);
goto fail;
}

创建内核命令行,调用grub_loader_set(),设置boot命令:

linux_cmdline = grub_zalloc (maximal_cmdline_size + 1);
if (!linux_cmdline)
goto fail;
grub_memcpy (linux_cmdline, LINUX_IMAGE, sizeof (LINUX_IMAGE));
{
grub_err_t err;
err = grub_create_loader_cmdline (argc, argv,
linux_cmdline
+ sizeof (LINUX_IMAGE) - 1,
maximal_cmdline_size
- (sizeof (LINUX_IMAGE) - 1),
GRUB_VERIFY_KERNEL_CMDLINE);
if (err)
goto fail;
}

len = prot_file_size;
if (grub_file_read (file, prot_mode_mem, len) != len && !grub_errno)
grub_error (GRUB_ERR_BAD_OS, N_("premature end of file %s"),
argv[0]);

if (grub_errno == GRUB_ERR_NONE)
{
grub_loader_set (grub_linux_boot, grub_linux_unload,
0 /* set noreturn=0 in order to avoid grub_console_fini() */);
loaded = 1;
}

grub_linux_boot

镜像位于grub-core\linux.mod。

负责完成内核参数传递、回收内存资源、调用ExitBootServices

static grub_err_t
grub_linux_boot (void)
{
grub_err_t err = 0;
const char *modevar;
char *tmp;
struct grub_relocator32_state state;
void *real_mode_mem;
struct grub_linux_boot_ctx ctx = {
.real_mode_target = 0
};
grub_size_t mmap_size;
grub_size_t cl_offset;
...
...

#ifdef GRUB_MACHINE_EFI
{
grub_efi_uintn_t efi_desc_size;
grub_size_t efi_mmap_target;
grub_efi_uint32_t efi_desc_version;

ctx.params->secure_boot = grub_efi_get_secureboot ();

err = grub_efi_finish_boot_services (&efi_mmap_size, efi_mmap_buf, NULL,
&efi_desc_size, &efi_desc_version);
if (err)
return err;

...

#endif

/* FIXME. */
/* asm volatile ("lidt %0" : : "m" (idt_desc)); */
state.ebp = state.edi = state.ebx = 0;
state.esi = ctx.real_mode_target;
state.esp = ctx.real_mode_target;
state.eip = ctx.params->code32_start;
return grub_relocator32_boot (relocator, state, 0);
}

来看看grub_efi_finish_boot_services,调用efi系统表中的exit_boot_services来将权限转移给内核:

grub_err_t
grub_efi_finish_boot_services (grub_efi_uintn_t *outbuf_size, void *outbuf,
grub_efi_uintn_t *map_key,
grub_efi_uintn_t *efi_desc_size,
grub_efi_uint32_t *efi_desc_version)
{
grub_efi_boot_services_t *b;
grub_efi_status_t status;

#if defined (__i386__) || defined (__x86_64__)
const grub_uint16_t apple[] = { 'A', 'p', 'p', 'l', 'e' };
int is_apple;

is_apple = (grub_memcmp (grub_efi_system_table->firmware_vendor,
apple, sizeof (apple)) == 0);
#endif

while (1)
{
if (grub_efi_get_memory_map (&finish_mmap_size, finish_mmap_buf, &finish_key,
&finish_desc_size, &finish_desc_version) < 0)
return grub_error (GRUB_ERR_IO, "couldn't retrieve memory map");

if (outbuf && *outbuf_size < finish_mmap_size)
return grub_error (GRUB_ERR_IO, "memory map buffer is too small");

finish_mmap_buf = grub_malloc (finish_mmap_size);
if (!finish_mmap_buf)
return grub_errno;

if (grub_efi_get_memory_map (&finish_mmap_size, finish_mmap_buf, &finish_key,
&finish_desc_size, &finish_desc_version) <= 0)
{
grub_free (finish_mmap_buf);
return grub_error (GRUB_ERR_IO, "couldn't retrieve memory map");
}

b = grub_efi_system_table->boot_services;
status = efi_call_2 (b->exit_boot_services, grub_efi_image_handle,
finish_key);
if (status == GRUB_EFI_SUCCESS)
break;

if (status != GRUB_EFI_INVALID_PARAMETER)
{
grub_free (finish_mmap_buf);
return grub_error (GRUB_ERR_IO, "couldn't terminate EFI services");
}

grub_free (finish_mmap_buf);
grub_printf ("Trying to terminate EFI services again\n");
}
grub_efi_is_finished = 1;
if (outbuf_size)
*outbuf_size = finish_mmap_size;
if (outbuf)
grub_memcpy (outbuf, finish_mmap_buf, finish_mmap_size);
if (map_key)
*map_key = finish_key;
if (efi_desc_size)
*efi_desc_size = finish_desc_size;
if (efi_desc_version)
*efi_desc_version = finish_desc_version;

#if defined (__i386__) || defined (__x86_64__)
if (is_apple)
stop_broadcom ();
#endif

return GRUB_ERR_NONE;
}

grub_memalign

align必须为2的幂

/* Magic words. */
#define GRUB_MM_FREE_MAGIC 0x2d3c2808
#define GRUB_MM_ALLOC_MAGIC 0x6db08fa4

参考

https://www.rodsbooks.com/efi-bootloaders/fallback.html

https://www.gnu.org/software/grub/manual/grub/html_node/Images.html#Images

https://wiki.ubuntu-tw.org/index.php?title=Grub2

https://www.cnblogs.com/f-ck-need-u/p/7094693.html#auto_id_17