I recently needed to set up a PC to dual boot Windows and Linux. Legacy BIOS, no UEFI. Unfortunately Windows likes to do Windows things and override the first-stage GRUB bootloader in the MBR with its own, and thereafter booting Windows only.
As this PC needs to be used by different persons with different technological skillsets, and in order to make repairing the boot process easier even if e.g. no Linux live boot medium were available, I came up with the following solution which loads GRUB via PXE to repair the "real" bootloader.
This solution is quite easy for end users. In the case of this specific PC, PXE boot can be triggered by keeping F8 pressed during boot. The instructions to do so fit on a small post-it note which I attached to the PC's screen.
Set Up DHCP and TFTP
- A configurable DHCP server
- A TFTP server
- A device with GRUB 2 already installed
All of those can of course be on the same device. Sometimes the first two even have to be, since a lot of consumer-grade network cards can only load data via TFTP from the same device that's running the TFTP server. Basically they ignore and don't request DHCP option 66 (server-name), and only handle DHCP option 67 (bootfile-name) and attempt to load it via TFTP from the address that offered the DHCP lease.
First we need to set up the TFTP server. I'll use dnsmasq for both
TFTP and DHCP. TFTP configuration in dnsmasq is as easy as the
following two lines in
We then need to configure a static DHCP lease with additional DHCP
options. Again in
# "pool" is the pool name dhcp-range=pool,192.168.0.10,192.168.0.254,24h # "target" is the target's host name. "tftp-grub" is a "tag" used in the next two lines. dhcp-host=01:23:45:67:89:0a,set:tftp-grub,192.168.0.10,target # Set TFTP server and bootfile path dhcp-option=tag:tftp-grub,option:tftp-server,192.168.0.1 dhcp-option=tag:tftp-grub,option:bootfile-name,/grub/i386-pc/core.0"
Create a GRUB 2 Netboot Environment
On the DHCP server (can also be done on a different host and then copied over), run the following command:
grub-mknetdir --net-directory /srv/tftp --subdir grub -d /usr/lib/grub/i386-pc/
The main difference between a system's
/boot/grub and this netboot
environment is the
grub/i386-pc/core.0 file. This is GRUB's
first-stage loader, the same thing that normally gets written to the
MBR, but here it's in a file instead, which can be loaded via TFTP.
If you now tell your target machine to boot from PXE, you should get a GRUB console.
Chainload GRUB 2 from GRUB 2
I searched around a long time to figure out how this is done, and most
of the results i got said to "just load the second GRUB's config file
into the first GRUB instance". This may work in most cases, but will
eventually break in this case, since the second GRUB's config file may
attempt to load additional modules from it's own prefix
/boot/grub/i386-pc), which will be incompatible with the PXE-loaded
GRUB if they're not the same version.
Long story short, unlike GRUB 1, GRUB 2's second-stage loader (the
core.img file) cannot be loaded on its own. Only the first-stage
loader (from MBR or
core.0) can be chainloaded. And since we're
trying to cover the case where the firs-stage loader has been
overwritten, we need to get it from somewhere else.
In this case I decided to set up a small loopback image in the target system's root partition which contained a copy of the system's GRUB installation.
To set up this loopback image, run the following commands on the target system:
dd if=/dev/zero of=/grubloop bs=100M count=1 parted /grubloop mklabel msdos # Offset from the start is needed so that there's enough space for # the first-stage loader. parted /grubloop mkpart primary ext4 1048576B 100% parted /grubloop set 1 boot on losetup -fo 1048576 /grubloop # Loop device number may be different mkfs.ext4 /dev/loop0 losetup -d /dev/loop0 mkdir /altboot mount -o offset=1048576 /grubloop /altboot
Also enter this into your
/grubloop /altboot ext4 loop,offset=1048576 0 0
Finally install GRUB to
grub-install --boot-directory=/altboot --target=i386-pc --force /grubloop grub-mkconfig -o /altboot/grub/grub.cfg
This image, which now contains the same boot config as your
/boot/grub, can now be booted like you'd boot any hard disk. To
chainload this GRUB image from the PXE grub, create the file
/srv/tftp/grub/grub.cfg on your TFTP server and add the following:
insmod biosdisk insmod part_msdos insmod ext2 insmod chain # Adapt to your real Linux root partition set root=(h0,msdos4) chainloader /grubloop +1 boot
I'm intentionally hardcoding the root partition here, instead of using
search module, since I want the PXE boot to be fast and
search module, on the other hand, loads a LOT of other
modules via TFTP, which may become a performance issue.
When you now boot your target device via PXE, it will load the first GRUB instance from TFTP, which will then chainload the second GRUB instance from the loopback image, which will then boot into Linux.
Automatically Re-Install GRUB
I'm doing all of the above not only to boot via PXE, but also to automatically re-install GRUB to the disk's MBR. I achieved this with the following systemd unit, which is executed on every boot of the Linux system:
[Unit] Description=Reinstall GRUB 2 to MBR and loopback altboot [Service] Type=oneshot ExecStart=/usr/sbin/grub-install --boot-directory=/altboot --target=i386-pc --force /grubloop ExecStart=/usr/sbin/grub-install --boot-directory=/boot --target=i386-pc /dev/disk/by-id/<DISK_ID> ExecStart=/usr/sbin/grub-mkconfig -o /altboot/grub/grub.cfg ExecStart=/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg [Install] WantedBy=multi-user.target
Maybe a bit overkill, but it gets the job done and is easy to use for end users.