GRUB 2 on a lower level

One of the nice things about the old venerable GRUB 1 was that is was quite simple and easily understandable, not to mention that its boot process was well-documented on a low level, which enabled debugging when stuff went wrong, and easy customization for out-of-the-ordinary scenarios. GRUB 2 is of course much nicer with its modular nature and much wider variety of features, but in its ambition to make everything work automatically, documentation on a lower level seems somewhat lacking. This is why I have been using GRUB 1 on some of my systems for many years since it was officially deprecated.

I finally decided to pull myself together on this and try to find out as much about GRUB 2 as I needed in order to regain the proficiency I felt I had with GRUB 1, and having browsed through the source code of the various utilities, this document is my attempt to document the conclusions partly for my own memory, but perhaps also for someone else who may be in a similar situation. This document does not aim to be any kind of complete manual to GRUB 2, however; its ambition is merely to augment the official manual with more low-level information.

Note that the structure of GRUB 2 seems to have been in a bit of flux, and that this document pertains to the post-1.99 versions of GRUB, where grub-install is not a shell script but a native C program, and the utility to install GRUB onto the MBR is called grub-bios-setup rather than grub-setup. It also only deals with BIOS booting, for now. If and when I configure some system to use UEFI booting, I may update this document to include information on that.

Some information in this document is guesswork. If there are errors, please drop me a mail and tell me about them.

The structure of GRUB 2

The structure of GRUB 1 was fairly simple; with its stages 1 & 2 and optional stage 1.5, there was not much to it. GRUB 2 is a bit more complex, but is mostly worth it. The main components of interest would seem to be:

The way GRUB is normally set up is by running grub-install and grub-mkconfig. grub-install will look at the general system configuration to determine the kinds of filesystems, virtual device layers and drivers to use, and use the information thus obtained to build a core image that contains the bare necessary modules that are needed for GRUB to load other modules, and install that core image to the specified hard drive. grub-mkconfig does similar things to construct a configuration file that loads the necessary modules to reach into the filesystem where the operating system kernel is located, load the kernel and actually boot.

The boot process

In the PC BIOS boot protocol, the BIOS selects (in an implementation-specific manner) a hard drive to boot from, and does so by loading the first 512-byte sector (the MBR) into a well-defined address, and finally jumping to the code thus loaded, letting it do the rest. Using GRUB, that code is the boot image as described above. When installed onto a device, the boot image is patched to contain the LBA address of the first sector of the core image (it relies on the core image always residing on the same disk as itself, the ID of which is supplied by the BIOS), and its task is to use BIOS calls to load that sector into memory and in turn transfer control to it.

In the i386-pc core image format, the first sector of the core image itself is patched with the LBA addresses of the rest of the blocks that together comprise the core image, and contains a loader that will load the rest of itself using that information. How the patching is done is described further below. Once the rest of the core image has been loaded into memory, the GRUB kernel takes over and initializes the modules that were packed alongside it into the core image. At this point, GRUB is basically running, and if anything goes wrong from hereon, it will drop into its rescue shell.

Once GRUB is thus running, the first thing it will do is to try and load the module called normal. When loading modules, GRUB uses the prefix string mentioned above as the base location, postfixed by the architecture name, in this case i386-pc. Module names are also postfixed with .mod to form a filename. Thus, if the prefix string is set to (hd0,msdos1)/boot/grub, then GRUB will attempt to locate the normal module by the complete name (hd0,msdos1)/boot/grub/i386-pc/normal.mod. The prefix string is also made available as the ${prefix} variable to GRUB commands. GRUB modules may specify dependencies of other modules, so loading normal will usually entail another couple of modules being loaded. Once GRUB has successfully loaded the normal module, it will run the normal command, which, perhaps obviously, comes from that module. From this point, everything is described by the GRUB manual.

The installation process

Unlike GRUB 1, GRUB 2 cannot install itself, which is perhaps a bit sad, but not a great loss. Instead, the user-space utilities that come with GRUB are used from within a booted operating system to do that task. Normally, the grub-install utility is used to do this task automatically, as described above, but this document will ignore that and focus on manual installation. This section will by necessity refer to certain files located on the host system, the location of which may differ from system to system; here, the locations on a standard Debian system are used. Modify as necessary.

Building a core image

Assuming the GRUB files and utilities are installed on your system, the first step in installing GRUB is to construct a core image, using grub-mkimage. The process is really quite simple: all that needs to be considered is the prefix string and the modules to be included. Normally, the module files and the pre-constructed images (particularly, the kernel image and the boot image) are located in /boot/grub/i386-pc on the host system. The files in there are simply copies of the GRUB installation in /usr/lib/grub/i386-pc, but the constructed core image is also usually put in the former directory, and will not exist in the latter.

The inputs to grub-mkimage, in total, are the path at which to put the constructed core image, the core image format, the prefix string, the modules to include, and, if not the default, the location of the module files. A complete command using the default module file location, therefore, may be:

grub-mkimage -O i386-pc -o /boot/grub/i386-pc/core.img -p '(hd0,msdos1)/boot/grub' biosdisk part_msdos ext2

This command includes the biosdisk, part_msdos and ext2 modules, in order for GRUB to be able to load additional modules from the real filesystem during boot. Additional modules to make GRUB even more self-contained may, of course, be included, but will grow the size of the core image, which may be an issue when embedding, as described below. If any of the modules specified depend on other modules, grub-mkimage will include them automatically.

Installation

Having a core image, the grub-bios-setup program is used to actually install GRUB on a boot device. Doing that involves three main steps.

First of all, GRUB 2 much prefers to try and embed the core image onto the boot device instead of letting the boot image load it directly from its location in the filesystem (unlike GRUB 1). There are many good reasons for this, not least of which being that GRUB 2 supports booting via some filesystems and/or block device layers that may move files around at whim, making its location in the filesystem unreliable. GRUB can be installed without embedding, but if embedding is possible, it won't even let you choose that option. When grub-bios-setup is run, it will examine the device it is instructed to install onto to see if embedding is possible. For MBR-partitioned disks, this means checking how much space is available between the MBR itself and the first partition. If the space available is greater than the size of the core image, it will copy the core image into those sectors; that is, starting with the second sector of the disk onwards. It is interesting in this context to note that Linux' fdisk tool has traditionally suggested starting the first partition on sector 63 of the disk, but has since switched to suggesting sector 2048 instead. One can only assume this is to make more space available for boot-loader embedding. GPT partition tables, on their hand, mandate a certain amount of space for embedding, which GRUB will happily use. If there is no space available, either due to the core image being too large, or due to the install device not having embeddable space at all (such as when installing into the boot sector of a partition with a filesystem rather than to the boot sector of the whole disk), grub-bios-setup will normally complain and exit, but can be run with the -f switch to skip embedding and use the core image where it resides in the filesystem.

When embedding the core image, grub-bios-setup will patch into it the sectors it was embedded into, leaving the original core image file intact and unchanged. If embedding is not used, grub-bios-setup will instead patch the core image file in-place with the sectors it is found to reside in on the filesystem.

Having embedded and/or patched the core image, grub-bios-setup will load the boot image, patch it with the first sector of the core image and also copy the existing partition table into it, and then write it to the installation device's first sector. At this point, installation is completed.

By default, grub-bios-setup only takes the installation device as an argument, and locates the core and boot images via the default names /boot/grub/i386-pc/core.img and /boot/grub/i386-pc/boot.img, respectively, but that can be changed using command-line options.

Recipe examples

Using the knowledge gained from the above sections, we can manually construct a USB drive containing a bootable GRUB installation. Since there are some buggy BIOSes that will look for certain legacy signatures in the MBR of a boot device that GRUB's boot image does not contain, we will use the install-mbr program to install a standard MBR onto the USB drive's boot sector, and instead install GRUB into the sole partition.

  1. Assuming the USB drive is /dev/sdb, partition the drive using fdisk to contain one large partition:
    $ sudo fdisk /dev/sdb
    Welcome to fdisk (util-linux 2.25.2).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.
    
    
    Command (m for help): o
    Created a new DOS disklabel with disk identifier 0xca031dff.
    	  
    Command (m for help): n
    Partition type
       p   primary (0 primary, 0 extended, 4 free)
       e   extended (container for logical partitions)
    Select (default p): p
    Partition number (1-4, default 1): 
    First sector (2048-3862527, default 2048): 
    Last sector, +sectors or +size{K,M,G,T,P} (2048-3862527, default 3862527): 
    
    Created a new partition 1 of type 'Linux' and of size 1.9 GiB.
    
    Command (m for help): t
    Selected partition 1
    Hex code (type L to list all codes): c
    Changed type of partition 'Linux' to 'W95 FAT32 (LBA)'.
    
    Command (m for help): a
    Selected partition 1
    The bootable flag on partition 1 is enabled now.
    
    Command (m for help): p
    Disk /dev/sdb: 1.9 GiB, 1977614336 bytes, 3862528 sectors
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0xca031dff
    
    Device     Boot Start     End Sectors  Size Id Type
    /dev/sdb1  *     2048 3862527 3860480  1.9G  c W95 FAT32 (LBA)
    
    
    Command (m for help): w
    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
  2. Then, mount the disk, copy the GRUB modules into a suitable directory (which, note, must at least include a directory named i386-pc, as described above for the module loading procedure):
    $ sudo mkfs.vfat /dev/sdb1 
    mkfs.fat 3.0.27 (2014-11-12)
    $ pmount sdb1
    $ cd /media/sdb1
    $ mkdir -p grub/i386-pc
    $ cp /usr/lib/grub/i386-pc/* grub/i386-pc/
  3. Create a core image to boot:
    $ grub-mkimage -O i386-pc -o grub/i386-pc/core.img -d /media/sdb1/grub/i386-pc -p '(hd0,msdos1)/grub' biosdisk part_msdos fat
    It is instructive to note that, when booting, the BIOS will always consider the disk actually booted from as the first disk, so (hd0) should always work as the device for the prefix string.
  4. Finally, install the GRUB boot image to the partition, and a standard MBR onto the device:
    $ sudo grub-bios-setup -f -d /media/sdb1/grub/i386-pc/ /dev/sdb1
    grub-bios-setup: warning: File system `fat' doesn't support embedding.
    grub-bios-setup: warning: Embedding is not possible.  GRUB can only be installed in this setup by using blocklists.  However, blocklists are UNRELIABLE and their use is discouraged..
    $ sudo install-mbr /dev/sdb
    Since we are installing GRUB directly onto the FAT partition, where there is no space for embedding, GRUB will complain and require the -f switch to proceed, as described above. Fortunately, FAT implementations do not normally move files around on the disk, so it is harmless until you decide to defrag the filesystem.
  5. Unmount the filesystem and it's all done!
Valid XHTML 1.1! Valid CSS! This site attempts not to be broken.
Author: Fredrik Tolf <fredrik@dolda2000.com>
Last changed: Sun Dec 17 07:18:02 2017