上一章重点分析了grub是如何探测一个文件系统的,本章分析grub-install的最后一部分代码,该代码包含了最主要的业务逻辑,下面来看。
grub2-install第六部分
util/grub-install.c
...
char mkimage_target[200];
const char *core_name = NULL;
switch (platform){
...
case GRUB_INSTALL_PLATFORM_I386_PC:
snprintf (mkimage_target, sizeof (mkimage_target), "%s-%s",
grub_install_get_platform_cpu (platform),
grub_install_get_platform_platform (platform));
core_name = "core.img";
break;
...
}
char *imgfile = grub_util_path_concat (2, platdir, core_name);
char *prefix = xasprintf ("%s%s", prefix_drive ? : "",
relative_grubdir);
grub_install_make_image_wrap (grub_install_source_directory,
prefix, imgfile, NULL, NULL, mkimage_target, 0);
...
前面的章节分析过,grub_install_get_platform_cpu函数最终返回i386,grub_install_get_platform_platform函数最终返回pc,通过snprintf函数拼接在一起,最终的mkimage_target字符串为i386-pc。设置imgfile为”/boot/grub/i386-pc/core.img”,prefix为”/boot/grub”,变量grub_install_source_directory为”/usr/local/lib/grub/i386-pc”,最后通过grub_install_make_image_wrap函数生成core.img。
grub2-install->grub_install_make_image_wrap
util/grub-install-common.c
void grub_install_make_image_wrap (const char *dir, const char *prefix,
const char *outname, char *memdisk_path,
char *config_path,
const char *mkimage_target, int note) {
FILE *fp;
fp = grub_util_fopen (outname, "wb");
grub_install_make_image_wrap_file (dir, prefix, fp, outname,
memdisk_path, config_path,
mkimage_target, note);
grub_util_file_sync (fp);
fclose (fp);
}
首先打开将要写入的文件”/boot/grub/i386-pc/core.img”,然后主体调用grub_install_make_image_wrap_file函数,最后的grub_util_file_sync函数将文件从缓存中同步到硬盘中。下面重点来看grub_install_make_image_wrap_file函数。
grub2-install->grub_install_make_image_wrap->grub_install_make_image_wrap_file
util/grub-install-common.c
void grub_install_make_image_wrap_file (const char *dir, const char *prefix,
FILE *fp, const char *outname,
char *memdisk_path,
char *config_path,
const char *mkimage_target, int note){
const struct grub_install_image_target_desc *tgt;
const char *const compnames[] = {
[GRUB_COMPRESSION_AUTO] = "auto",
[GRUB_COMPRESSION_NONE] = "none",
[GRUB_COMPRESSION_XZ] = "xz",
[GRUB_COMPRESSION_LZMA] = "lzma",
};
grub_size_t slen = 1;
char *s, *p;
char **pk, **md;
int dc = decompressors ();
for (md = modules.entries; *md; md++){
slen += 10 + grub_strlen (*md);
}
p = xmalloc (slen);
for (md = modules.entries; *md; md++) {
*p++ = '\'';
p = grub_stpcpy (p, *md);
*p++ = '\'';
*p++ = ' ';
}
*p = '\0';
tgt = grub_install_get_image_target (mkimage_target);
grub_install_generate_image (dir, prefix, fp, outname,
modules.entries, memdisk_path,
pubkeys, npubkeys, config_path, tgt,
note, compression);
while (dc--)
grub_install_pop_module ();
}
decompressors函数根据压缩函数指针compress_func加入相应的压缩模块。接下来将全局变量modules中的字符串添加到新分配的内存空间p中。grub_install_get_image_target根据cpu体系mkimage_target获得对应的grub_install_image_target_desc结构,然后调用grub_install_generate_image函数生成core.img,该函数的《grub-mkimage源码分析》文章中分析了。最后通过grub_install_pop_module函数将decompressors函数中添加的模块删除。
grub2-install->grub_install_make_image_wrap->grub_install_make_image_wrap_file->decompressors
util/grub-install-common.c
static int decompressors (void) {
if (compress_func == grub_install_compress_gzip){
grub_install_push_module ("gzio");
return 1;
}
if (compress_func == grub_install_compress_xz){
grub_install_push_module ("xzio");
grub_install_push_module ("gcry_crc");
return 2;
}
if (compress_func == grub_install_compress_lzop){
grub_install_push_module ("lzopio");
grub_install_push_module ("adler32");
grub_install_push_module ("gcry_crc");
return 3;
}
return 0;
}
decompressors函数就是根据compress_func函数指针,选择需要向core.img映像中添加的模块,例如压缩函数为grub_install_compress_gzip时,就需要添加gzio模块。
grub2-install->grub_install_make_image_wrap->grub_install_make_image_wrap_file->grub_install_get_image_target
util/mkimage.c
const struct grub_install_image_target_desc * grub_install_get_image_target (const char *arg){
unsigned i, j;
for (i = 0; i < ARRAY_SIZE (image_targets); i++)
for (j = 0; j < ARRAY_SIZE (image_targets[i].names) &&
image_targets[i].names[j]; j++)
if (strcmp (arg, image_targets[i].names[j]) == 0)
return &image_targets[i];
return NULL;
}
grub_install_get_image_target函数比较cpu体系字符串arg,如果和全局的image_targets数组中的每个项的name字段对应,就返回该项。假设参数arg为i386-pc,最终返回的结构如下
{
.dirname = "i386-pc",
.names = { "i386-pc", NULL },
.voidp_sizeof = 4,
.bigendian = 0,
.id = IMAGE_I386_PC,
.flags = PLATFORM_FLAGS_DECOMPRESSORS,
.total_module_size = TARGET_NO_FIELD,
.decompressor_compressed_size = GRUB_DECOMPRESSOR_I386_PC_COMPRESSED_SIZE,
.decompressor_uncompressed_size = GRUB_DECOMPRESSOR_I386_PC_UNCOMPRESSED_SIZE,
.decompressor_uncompressed_addr = TARGET_NO_FIELD,
.section_align = 1,
.vaddr_offset = 0,
.link_addr = GRUB_KERNEL_I386_PC_LINK_ADDR,
.default_compression = GRUB_COMPRESSION_LZMA
}
grub2-install第七部分
util/grub-install.c
...
switch (platform){
case GRUB_INSTALL_PLATFORM_I386_PC:
char *boot_img_src = grub_util_path_concat (2,
grub_install_source_directory, "boot.img");
char *boot_img = grub_util_path_concat (2, platdir, "boot.img");
grub_install_copy_file (boot_img_src, boot_img, 1);
if (install_bootsector)
grub_util_bios_setup (platdir, "boot.img", "core.img",
install_drive, force,
fs_probe, allow_floppy, add_rs_codes);
break;
...
}
grub_gcry_fini_all ();
grub_fini_all ();
return 0;
}
继续往下看,接下来获取boot.img所在路径boot_img_src,默认为”/usr/local/lib/grub/i386-pc/boot.img”,boot_img为该文件需要拷贝到的路径,默认为”/boot/grub/i386-pc/boot.img”。然后就通过grub_install_copy_file函数将boot_img_src处的boot.img拷贝到boot_img路径中。
变量install_bootsector默认为1,因此接下来通过grub_util_bios_setup函数执行grub文件的安装操作。最后调用grub_gcry_fini_all和grub_fini_all函数做一些收尾工作,本章不关心。
下面重点分析grub_util_bios_setup函数,该函数较长,分为多个部分分析。
grub_util_bios_setup第一部分
util/setup.c
void SETUP (const char *dir, const char *boot_file, const char *core_file,
const char *dest, int force, int fs_probe, int allow_floppy,
int add_rs_codes __attribute__ ((unused))) {
char *core_path;
char *boot_img, *core_img, *boot_path;
char *root = 0;
size_t boot_size, core_size;
grub_uint16_t core_sectors;
grub_device_t root_dev = 0, dest_dev, core_dev;
grub_util_fd_t fp;
struct blocklists bl;
bl.first_sector = (grub_disk_addr_t) -1;
bl.current_segment = GRUB_BOOT_I386_PC_KERNEL_SEG + (GRUB_DISK_SECTOR_SIZE >> 4);
bl.last_length = 0;
boot_path = grub_util_get_path (dir, boot_file);
boot_size = grub_util_get_image_size (boot_path);
boot_img = grub_util_read_image (boot_path);
free (boot_path);
core_path = grub_util_get_path (dir, core_file);
core_size = grub_util_get_image_size (core_path);
core_sectors = ((core_size + GRUB_DISK_SECTOR_SIZE - 1) >> GRUB_DISK_SECTOR_BITS);
core_img = grub_util_read_image (core_path);
bl.first_block = (struct grub_boot_blocklist *) (core_img
+ GRUB_DISK_SECTOR_SIZE
- sizeof (*bl.block));
...
current_segment定义为0x820,其实是后面解压缩程序的装载地址。boot_path为grub目录下boot.img所在路径,默认为”/boot/grub/i386-pc/boot.img”。然后通过grub_util_get_image_size函数获取该文件大小boot_size,必须为512字节。,再通过grub_util_read_image函数读取该文件到内存boot_img中。
同理,core_path为/boot/grub/i386-pc/core.img,该文件由前面的grub_install_generate_image函数生成,内部包含了diskboot.img,lzma_decompress.img和grub的核心代码kernel.img。core_size为core.img文件的大小,对应的扇区数core_sectors。最后读取该文件到内存core_img中。
core.img的前512字节为diskboot.img,由diskboot.S编译而来,first_block指向diskboot.S文件中的标号blocklist_default_start处,该标号处于grub_boot_blocklist数组最后一项的起始地址,grub_boot_blocklist内部保存了如何从硬盘读取后续文件的信息。
grub_util_bios_setup第二部分
util/setup.c
...
dest_dev = grub_device_open (dest);
core_dev = dest_dev;
char **root_devices = grub_guess_root_devices (dir);
char **cur;
int found = 0;
for (cur = root_devices; *cur; cur++){
char *drive;
grub_device_t try_dev;
drive = grub_util_get_grub_dev (*cur);
try_dev = grub_device_open (drive);
if (!found && try_dev->disk->id == dest_dev->disk->id
&& try_dev->disk->dev->id == dest_dev->disk->dev->id){
if (root_dev)
grub_device_close (root_dev);
free (root);
root_dev = try_dev;
root = drive;
found = 1;
continue;
}
if (!root_dev){
root_dev = try_dev;
root = drive;
continue;
}
grub_device_close (try_dev);
free (drive);
}
for (cur = root_devices; *cur; cur++)
free (*cur);
free (root_devices);
grub_env_set ("root", root);
...
传入的参数dest是要将grub安装到的硬盘,例如hd0。参数dir是grub文件所在目录,例如”/boot/grub/i386-pc/”。首先通过grub_guess_root_devices函数获得grub文件夹所在的根设备,返回对应的设备文件,例如”/dev/sdb”。
接下来遍历可能的根设备文件root_devices,调用grub_util_get_grub_dev函数获得对应的设备,例如hd0。调用grub_device_open打开该设备,获取设备参数,保存在try_dev中。
接下来设置根设备root_dev为新打开的设备try_dev,设置环境变量root指向根设备,例如hd0,如果grub目录所在的根设备try_dev和将要安装的根设备dest_dev相同,就设置found为1。最后设置环境变量root并返回。
grub_util_bios_setup第三部分
util/setup.c
...
char *tmp_img;
grub_uint8_t *boot_drive_check;
tmp_img = xmalloc (GRUB_DISK_SECTOR_SIZE);
grub_disk_read (dest_dev->disk, 0, 0, GRUB_DISK_SECTOR_SIZE, tmp_img);
boot_drive_check = (grub_uint8_t *) (boot_img
+ GRUB_BOOT_MACHINE_DRIVE_CHECK);
memcpy (boot_img + GRUB_BOOT_MACHINE_BPB_START,
tmp_img + GRUB_BOOT_MACHINE_BPB_START,
GRUB_BOOT_MACHINE_BPB_END - GRUB_BOOT_MACHINE_BPB_START);
if (!allow_floppy && !grub_util_biosdisk_is_floppy (dest_dev->disk)){
boot_drive_check[0] = 0x90;
boot_drive_check[1] = 0x90;
}
struct identify_partmap_ctx ctx = {
.dest_partmap = NULL,
.container = dest_dev->disk->partition,
.multiple_partmaps = 0
};
int is_ldm;
grub_err_t err;
grub_disk_addr_t *sectors;
int i;
grub_fs_t fs;
unsigned int nsec, maxsec;
grub_partition_iterate (dest_dev->disk, identify_partmap, &ctx);
...
fs = grub_fs_probe (dest_dev);
is_ldm = grub_util_is_ldm (dest_dev->disk);
...
首先读取目的设备dest_dev的引导扇区至新分配的内存tmp_img中。根据GRUB_BOOT_MACHINE_DRIVE_CHECK偏移将boot_drive_check指向boot.S文件的标号boot_drive_check处。首先拷贝BIOS参数到tmp_img中,allow_floppy表示是否允许从软盘启动,如果不允许,并且grub_util_biosdisk_is_floppy函数检测出目的设备dest_dev不是软盘,则改变boot.img中boot_drive_check偏移处的起始jmp语句为nop(0x90),jmp语句之下会检测设备是否为硬盘,即是否大于0x80,如果小于0x80,则此时将设备号设置为0x80。
接着通过grub_partition_iterate函数遍历分区,假设最后返回的ctx中的dest_partmap保存了分区类型,例如grub_msdos_partition_map。往下省略的部分是对分区进行检查,grub_fs_probe探测文件系统,假设为ext2。
grub_util_is_ldm和硬盘的LVM相关,这里假设最后返回false。
grub_util_bios_setup第四部分
util/setup.c
if (fs_probe){
...
if (ctx.dest_partmap && !ctx.dest_partmap->embed){
goto unable_to_embed;
}
..
}
unable_to_embed:
grub_fs_t fs;
fs = grub_fs_probe (root_dev);
...
bl.block = bl.first_block;
while (bl.block->len){
bl.block->start = 0;
bl.block->len = 0;
bl.block->segment = 0;
bl.block--;
}
bl.block = bl.first_block;
grub_install_get_blocklist (root_dev, core_path, core_img, core_size,
save_blocklists, &bl);
write_rootdev (root_dev, boot_img, bl.first_sector);
fp = grub_util_fd_open (core_path, GRUB_UTIL_FD_O_WRONLY);
grub_util_fd_write (fp, core_img, GRUB_DISK_SECTOR_SIZE * 2);
grub_util_fd_sync (fp);
grub_util_fd_close (fp);
grub_util_biosdisk_flush (root_dev->disk);
grub_disk_cache_invalidate_all ();
...
grub_disk_write (dest_dev->disk, BOOT_SECTOR,
0, GRUB_DISK_SECTOR_SIZE, boot_img);
grub_util_biosdisk_flush (root_dev->disk);
grub_util_biosdisk_flush (dest_dev->disk);
free (core_path);
free (core_img);
free (boot_img);
grub_device_close (dest_dev);
grub_device_close (root_dev);
}
假设上面一个函数grub_partition_iterate函数返回的分区类型为grub_msdos_partition_map,此时直接跳到unable_to_embed标号处执行。然后获得grub目录对应根设备的文件系统fs。
bl的block字段指向diskboot.S源文件中blocklist数组的最后一个block处。最后一个block(也即first_block)的len变量记录了需要的block大小,该值在grub_install_generate_image函数的最后设置(《grub-mkimage源码分析—3》)。根据len值,通过while循环初始化blocklist数组。然后调用grub_install_get_blocklist函数根据core_img的大小设置该blocklist数组,内部调用函数指针save_blocklists将其写入内存core_img。
接下来调用write_rootdev更新boot.img文件在内存中的映像boot_img,first_sector表示core.img在硬盘上第一个扇区的扇区号,也即diskboot.S代码所在的扇区起始号。再往下以只写方式打开”/boot/grub/i386-pc/core.img”文件,再通过grub_util_fd_write函数向其中写入core_img的前两个扇区。其中第一个扇区是diskboot.img。根据前面的分析,这里写入一个扇区也行,因为前面的操作只修改了第一个扇区,也即diskboot.img中的值,写入两个扇区应该是考虑了其他不同的情况,这里不往下深究了。
省略的部分是对刚刚写入硬盘的core.img作检查,其做法是首先读取第一个扇区,也即diskboot.img,然后根据diskboot.img最后部分的blocklist,依次读取接下来的扇区,然后比较一共读取的数据长度和总长度core_size,如果不相等,则说明blocklist发生了错误。
接下来通过grub_disk_write函数将boot_img写入根设备的主引导扇区中,传入的宏定义BOOT_SECTOR为0标识了该引导扇区为主引导扇区。最后同步文件,关闭设备。
grub_util_bios_setup->grub_install_get_blocklist
util/setup.c
void grub_install_get_blocklist (grub_device_t root_dev, const char *core_path,
const char *core_img __attribute__ ((unused)),
size_t core_size,
void (*callback) (grub_disk_addr_t sector,
unsigned offset,
unsigned length,
void *data),
void *hook_data){
grub_partition_t container = root_dev->disk->partition;
grub_uint64_t container_start = grub_partition_get_start (container);
struct fiemap fie1;
int fd;
fd = open (core_path, O_RDONLY);
grub_memset (&fie1, 0, sizeof (fie1));
fie1.fm_length = core_size;
fie1.fm_flags = FIEMAP_FLAG_SYNC;
if (ioctl (fd, FS_IOC_FIEMAP, &fie1) < 0){
int nblocks, i;
int bsize;
int mul;
ioctl (fd, FIGETBSZ, &bsize);
mul = bsize >> GRUB_DISK_SECTOR_BITS;
nblocks = (core_size + bsize - 1) / bsize;
for (i = 0; i < nblocks; i++){
unsigned blk = i;
int rest;
ioctl (fd, FIBMAP, &blk);
rest = core_size - ((i * mul) << GRUB_DISK_SECTOR_BITS);
if (rest <= 0)
break;
if (rest > GRUB_DISK_SECTOR_SIZE * mul)
rest = GRUB_DISK_SECTOR_SIZE * mul;
callback (((grub_uint64_t) blk) * mul
+ container_start, 0, rest, hook_data);
}
}else{
...
}
close (fd);
}
首先获得grub目录所在分区container,通过grub_partition_get_start函数获得该分区的起始地址container_start。然后通过参数为FS_IOC_FIEMAP的ioctl判断文件系统的block管理方式,else部分表示使用extent来管理,即不使用block或者说block bitmap来管理block,这里不关心。下面只看if部分的代码。
首先通过参数为FIGETBSZ的ioctl函数获得文件系统一个block的大小bsize,mul表示一个block大小占用多少扇区数,nblocks表示core.img的大小占用多少block。再通过参数为FIBMAP的ioctl函数根据逻辑块号i获取物理块号,保存在blk中。变量rest表示grub一次读取的block数,默认为一次读取一个block。
参数callback函数指针指向save_blocklists,调用该函数设置内存映像core_img中的blocklist。
grub_util_bios_setup->grub_install_get_blocklist->save_blocklists
util/setup.c
static void save_blocklists (grub_disk_addr_t sector, unsigned offset,
unsigned length, void *data){
struct blocklists *bl = data;
struct grub_boot_blocklist *prev = bl->block + 1;
grub_uint64_t seclen;
if (bl->first_sector == (grub_disk_addr_t) -1){
bl->first_sector = sector;
sector++;
length -= GRUB_DISK_SECTOR_SIZE;
if (!length)
return;
}
seclen = (length + GRUB_DISK_SECTOR_SIZE - 1) >> GRUB_DISK_SECTOR_BITS;
if (bl->block != bl->first_block && (grub_target_to_host64 (prev->start)
+ grub_target_to_host16 (prev->len)) == sector){
grub_uint16_t t = grub_target_to_host16 (prev->len);
t += seclen;
prev->len = grub_host_to_target16 (t);
}else{
bl->block->start = grub_host_to_target64 (sector);
bl->block->len = grub_host_to_target16 (seclen);
bl->block->segment = grub_host_to_target16(bl->current_segment);
bl->block--;
}
bl->last_length = length & (GRUB_DISK_SECTOR_SIZE - 1);
bl->current_segment += seclen << (GRUB_DISK_SECTOR_BITS - 4);
}
sector表示本次读取的扇区起始地址,length是一次读取的字节长度,seclen是length对应的扇区长度,prev表示上一个block结构的地址。if语句里表示的是文件的内容在物理block上是连续的,此时只要增加上一个block结构的len变量即可。
进入else部分就表示文件的内容在物理block上不连续,或者是第一次进入进入该函数,即此时block和firstblock相等,此时设置start表示绝对扇区标号,len是读取的以扇区为单位的长度,segment是将这段文件内容装在到内存时的段地址。last_length是最后一个扇区的字节长度。
grub_util_bios_setup->write_rootdev
util/setup.c
static void write_rootdev (grub_device_t root_dev, char *boot_img,
grub_uint64_t first_sector){
grub_uint8_t *boot_drive;
void *kernel_sector;
boot_drive = (grub_uint8_t *) (boot_img + GRUB_BOOT_MACHINE_BOOT_DRIVE);
kernel_sector = (boot_img + GRUB_BOOT_MACHINE_KERNEL_SECTOR);
*boot_drive = 0xFF;
grub_set_unaligned64 (kernel_sector, grub_cpu_to_le64 (first_sector));
}
首先初始化boot_img中的GRUB_BOOT_MACHINE_BOOT_DRIVE地址处的值标识启动硬盘,GRUB_BOOT_MACHINE_KERNEL_SECTOR宏表示diskboot映像所在的扇区。