В статье о возможности загружать Linux с VHD был предложен способ загружать Linux на машине с Windows без необходимости разбивать диск на разделы. Но было одно существенное ограничение: рассматривался только вариант загрузки BIOS, т.е. legacy-загрузки. Сейчас появляется всё больше устройств без поддержки режима legacy (например, многие ноутбуки, выпущенные в 2020 году). Поэтому в этой статье будет рассмотрена загрузка Linux с VHD на компьютерах с UEFI.

В чём отличия UEFI от BIOS, в интернете описано достаточно подробно, можно посмотреть, например, в этой статье. Для нас наиболее важным будет необходимость использовать разметку GPT для загрузочного диска и при создании разделов на VHD. Эксперименты проводились при отключенном Secure Boot. Не буду подробно останавливаться на моментах, описанных в предыдущей части. Как и ранее, предполагается, что читатель имеет представление о работе с консолью в Windows и Linux, умеет работать со стандартными системными утилитами, с ПО для виртуализации и т.п.

Предварительно отмечу еще ряд моментов. Можно было бы рассмотреть загрузку системы с внутреннего жесткого диска компьютера, но мы немного усложним задачу и будем загружать систему со съемного USB-диска, на котором будет располагаться VHD-файл. Все действия применимы и для внутреннего жесткого диска, а некоторые особенности будут отмечены в отдельном разделе.

Разработчики grub4dos недавно выпустили версию своего загрузчика для UEFI. При использовании версии grub4dos-for_UEFI-2021-02-10.7z на виртуальной машине у меня не возникло особых проблем при загрузке Linux с VHD, но на реальном железе при работе с тем же самым образом VHD была получена ошибка: Error 24: Attempt to access block outside of partition. С учетом простоты, с которой UEFI позволяет заменить загрузчик (простая замена файла), было решено воспользоваться загрузчиком grub2 из дистрибутива Debian debian-10.8.0-amd64-netinst.iso.

Установка Linux на VHD

Есть два ключевых отличия от варианта для BIOS:

1) в настройках VirtualBox необходимо установить опцию "Enable EFI";

Настройки VirtualBox

2) при установке Debian диск был разбит на два раздела: ESP (EFI system partition) размером ~100 Мб, отформатированный в FAT32, и EXT4, занимающий всё оставшееся место.

Разметка дисков

Возможны и другие варианты разбиения. Раздел ESP при желании можно вынести за пределы VHD (он нам нужен только для загрузки на виртуальной машине), но все дальнейшие настройки сделаны исходя из того, что на VHD будет два раздела.

Подготовка Linux к загрузке с VHD

Скрипт из предыдущей статьи нам подходит для загрузки на локальном компьютере, где состав дисков меняется редко и заранее известно, на каком диске хранится VHD-файл. Но мы себе поставили новую цель: иметь возможность загружаться с USB-носителя на любом компьютере с любой конфигурацией жестких дисков. Загрузчики grub4dos и grub2 умеют искать файлы на доступных разделах и работать с UUID разделов. Мы этим воспользуемся при создании конфигурационного файла загрузчика, а пока добавим обработку UUID в скрипт loop_boot_vhd.

loop_boot_vhd
#!/bin/sh

PREREQ=""
prereqs()
{
	echo "$PREREQ"
}
case $1 in
# get pre-requisites
prereqs)
	prereqs
	exit 0
	;;
esac

cmdline=$(cat /proc/cmdline)

for x in $cmdline; do
	value=${x#*=}
	case ${x} in
		root=*) value_loop_check=${value#*loop}
			if [ x$value_loop_check != x$value ]
				then loop_dev=${value%p*}
				loop_part_num=${value##*p}
				else echo "Root device is not a loop device. loopboot hook will be terminated."; return
			fi ;;
		loop_file_path=*) loop_file_path=$value ;;
		loop_dev_path=*) loop_dev_path=$value ;;
		loop_dev_uuid=*) loop_dev_uuid=$value ;;
	esac
done

if [ -z $loop_file_path ] || ([ -z $loop_dev_path ] && [ -z $loop_dev_uuid ])
	then echo "Either loop_file_path or loop_dev_path/loop_dev_uuid parameter does not specified. loopboot hook will be terminated."; return
fi
modprobe loop max_part=64 max_loop=8
for n in 1 2 3 4 5 6 7 8 9 10; do
	if [ ! -z $loop_dev_uuid ]; then
		loop_dev_path=$(findfs UUID=$loop_dev_uuid)
	fi
	test_usb_ready=$(ls $loop_dev_path 2>&1)
	if [ "$loop_dev_path" != "$test_usb_ready" ]; then
		echo "wait for $loop_dev_path ($loop_dev_uuid): $n"
		sleep 1
	else
		break
	fi
done
loop_dev_type=$(blkid -s TYPE -o value $loop_dev_path)
if [ ! -d /host ]; then
	mkdir /host
fi
if [ "$loop_dev_type" != "ntfs" ]
	then mount -t $loop_dev_type $loop_dev_path /host; echo "mount -t $loop_dev_type $loop_dev_path /host"; echo "mounted using mount";
	else ntfs-3g $loop_dev_path /host; echo "mounted using ntfs-3g";
fi
losetup $loop_dev /host$loop_file_path

В скрипт добавлено считывание нового параметра loop_dev_uuid. В нём ожидается UUID раздела, на котором находится VHD-файл (путь к нему передается, как и раньше, в параметре loop_file_path). Если задан loop_dev_uuid, то значение, переданное в loop_dev_path, игнорируется и заменяется на найденный по UUID путь к устройству. Кроме того, если диск подключен по USB, то он может определяться в системе с задержкой. Поэтому добавлено ожидание (до 10 секунд), во время которого выполняется ежесекундная проверка, появилось нужное устройство или нет. Мы ищем раздел по UUID с помощью утилиты findfs. Она не входит в образ initramfs, для её добавления необходимо создать скрипт /etc/initramfs-tools/hooks/copyfindfs (не забывайте делать скрипты исполняемыми).

copyfindfs
#!/bin/sh
cp -p /usr/sbin/findfs "$DESTDIR/bin/findfs"

Скрипт loop_boot_vhd необходимо расположить, как и ранее, в /etc/initramfs-tools/scripts/local-top/. После чего пересобрать initramfs, и VHD-образ можно считать готовым.

update-initramfs -c -k all

Настройка grub.cfg

В GRUB2 можно получить UUID раздела с помощью модуля probe, поэтому он должен быть в сборке. За основу была взята сборка GRUB2 с установочного диска Debian, для загрузки нам понадобятся следующие файлы:

\EFI\boot\bootx64.efi
\EFI\boot\grubx64.efi
\boot\grub\x86_64-efi\probe.mod

Их необходимо скопировать на EFI-раздел USB-диска:

\EFI\boot\bootx64.efi
\EFI\boot\grubx64.efi
\EFI\debian\x86_64-efi\probe.mod

Далее необходимо создать конфигурационный файл \EFI\debian\grub.cfg.

grub.cfg
menuentry "vhdUUID" {
  insmod probe
  set vhd_name="/debefi.vhd"
  search --no-floppy --set=vhd_dev --file $vhd_name
  probe -u $vhd_dev --set=vhd_uuid
  loopback loop ($vhd_dev)$vhd_name
  linux  (loop,gpt2)/boot/vmlinuz-4.19.0-14-amd64 root=/dev/loop0p2 rw loop_file_path=$vhd_name loop_dev_uuid=$vhd_uuid
  initrd (loop,gpt2)/boot/initrd.img-4.19.0-14-amd64
}

В cfg-файле всё достаточно очевидно, только обращу внимание, что VHD-файл имеет название debefi.vhd, и система будет искать его в корне всех найденных разделов. Для исключения ошибок название файла необходимо сделать уникальным для загружаемой системы. Ну и "gpt2" используется потому, что Linux установлен на втором разделе внутри VHD.

Особенности настройки параллельно с Windows bootloader

Disclaimer: дальнейшие действия могут привести к невозможности загрузить компьютер штатными средствами, все действия вы выполняете на свой страх и риск. Перед экспериментом проверьте, контролирует ли установленное антивирусное ПО целостность загрузчика. И не забывайте о возможных побочных эффектах.

Я опишу один из вариантов реализации выбора между загрузкой Windows и Linux: с помощью замены загрузчика на GRUB2. Загрузка EFI-систем по умолчанию начинается с файла \EFI\Boot\BOOTX64.EFI, в Windows 10 используется \EFI\Microsoft\Boot\bootmgfw.efi. Заменить загрузчик Microsoft на GRUB2 можно просто заменив файл. Необходимо быть внимательным, если сомневаетесь в получении результата с первого раза, то лучше сначала поэкспериментируйте на виртуальной машине. С помощью diskpart.exe надо подключить EFI-раздел Windows, на котором необходимо:

  • переименовать/перенести файл \EFI\Microsoft\Boot\bootmgfw.efi в \EFI\boot\ms.efi;

  • файл bootx64.efi из ISO-образа Debian переименовать в \EFI\Microsoft\Boot\bootmgfw.efi;

  • grubx64.efi разместить в \EFI\Microsoft\Boot\grubx64.efi;

  • probe.mod разместить в \EFI\debian\x86_64-efi\probe.mod;

  • ранее созданный grub.cfg скопировать в \EFI\debian\grub.cfg и добавить пункт для передачи управления загрузчику Microsoft:

grub.cfg
menuentry "ms" {
  chainloader /EFI/boot/ms.efi
}

Теперь при загрузке сначала будет появляться меню GRUB2 с выбором загрузки Windows ("ms") или Linux ("vhdUUID").

Если на каком-то из этапов загрузки возникают ошибки, то (как и с grub4dos) необходимо попытаться вводить команды вручную и смотреть, какие ошибки выдает система, как и куда монтируются разделы, доступны ли все необходимые файлы и т.п.