请注意,本文中的方法已经不再被建议使用。因为在 2021 年 9 月 5 日,随着 OpenWrt 21.02 的释出,官方已经正式开始提供支持 EFI 的镜像,只需要下载带 -efi 后缀的即可,如 generic-ext4-combined-efi.img.gz
。
可惜这篇文章只活了半年就结束了它的使命(主要还是我咕,不然早就可以出现了)。
在 Hyper-V 上第一代和第二代虚拟机有着一些差别,最显著的就是旧式 BIOS 启动和 UEFI 启动的区别,但还有其他的例如热插拔硬盘和网卡的区别。我一直以为是 OpenWrt 对 UEFI 启动支持不良,官方才一直没有推出 EFI 的磁盘映像的。因此,之前我的做法都是创建好一个 OpenWrt 虚拟机后都一次性加一堆网卡,以解决临时要加一个口的问题。昨天准备给家里的 OpenWrt 升级版本,就又想到了这个事情,就想试一试如果用 UEFI 启动能不能正常使用。
OpenWrt x86 的启动本身就是 GRUB 了,按理说如果内核支持,那么只要整一个 ESP 分区,然后整个 EFI 版本的 GRUB 进去,grub.cfg 复制过来以后按照老样子启动就可以了。试了之后,确实就这么简单就能启动并正常使用了,暂时也没发现什么问题,就让它冒烟测试吧。因为在调研的时候似乎没看到介绍这么做的教程,我就来水一篇博客了。
为了方便使用 GRUB 的工具,本文所有操作均在 Debian bullseye 上完成。那么接下来我将介绍如何构建同时支持 MBR 启动和 UEFI 启动的 OpenWrt 磁盘映像。
因为我也担心遇到 UEFI 下的问题,为了能够回滚的考虑,就选择了构建一个支持 MBR 和 UEFI 启动的版本,可以在保证旧式 MBR 启动的模式兼容的前提下,支持 UEFI 启动。注意,你同时需要 MBR 和 EFI 两种 GRUB 的文件,Debian 中对应的是 grub-efi-amd64-bin 和 grub-pc-bin 两个包。
首先先从 https://downloads.openwrt.org/ 上下载得到最新的磁盘映像,目前为 openwrt-19.07.7-x86-64-combined-ext4.img.gz,并解压得到磁盘映像。
接着使用 losetup -f
,获得一个可以让我们挂载磁盘映像的 loop 设备,并用 losetup -P /dev/loop0 openwrt-19.07.7-x86-64-combined-ext4.img
将磁盘映像关联到这个 loop 设备。
然后我们将 boot 分区(即第一个分区)挂载到一个文件夹,直接把里面的 boot 分区里面的 boot 文件夹复制出来,之后我们会将它塞回新的 ESP 分区。
复制出来以后,先用 umount 卸载,然后使用 fdisk /dev/loop0
,将第一个原来的启动分区更改类型为 EFI。先输入“t”(修改分区类型),然后输入“1”(第一个分区),然后输入“uefi”或者“ef”,再使用“w”将改动写入分区表。
ESP 分区本质是一个 FAT12/16/32 的分区,而官方磁盘映像中第一个分区仅 16M,因此我们需要使用 mkfs.fat -n EFI -F16 /dev/loop0p1
将第一个分区格式化为 FAT16。
改造好 ESP 分区后,重新将这个分区挂载上,并将之前复制出来的 boot 文件夹,复制回这个分区,并建立 EFI/BOOT
(大小写不敏感)文件夹,还要将 configfile ${cmdpath}/../../boot/grub/grub.cfg
这句话写入 image.cfg 文件中。
这些准备就绪后,就是创建 GRUB 的 EFI 程序了。使用 grub-mkstandalone -d /usr/lib/grub/x86_64-efi/ -O x86_64-efi --locales="" --themes="" --fonts="" --modules="part_msdos" -o esp/EFI/BOOT/BOOTX64.EFI "boot/grub/grub.cfg=image.cfg"
这句话就可以生成 GRUB 的 EFI 文件了。
因为 EFI 版本的 GRUB 中,会先将 GRUB 所用到的文件,例如 modules、locales、themes 之类的文件塞到 EFI 程序里,在运行的时候将其挂载为一个内存盘,所以第一时间使用的 grub.cfg 也来自这个内存盘。这里最后一个参数的意思是将“image.cfg”这个文件作为这个内存盘里面的 grub.cfg 来写入。而在 image.cfg 中,我们使用了“configfile”再次将配置文件指向 ESP 分区中的 boot/grub/grub.cfg。cmdpath 代表 GRUB 的 EFI 文件所在的目录。而 modules 那一个参数的意思则是让 GRUB preload 了 part_msdos 模块,不然它无法正常读取 MBR 分区。你可以查看文档,了解更多例如 locales、themes、fonts 之类的参数(这些这里都设为空了,默认的 all 会使 EFI 文件巨大)的用法,并随你的想法增减。
UEFI 模式的 GRUB 完成了,因为我们修改了分区类型,接下来还要重新安装 MBR 的 GRUB。我们使用 grub-install --target=i386-pc --locales="" --themes="" --fonts="" --boot-directory=esp/boot /dev/loop0
来完成这个工作。
最后再使用 umount 卸载挂载的分区,使用 losetup -d /dev/loop0
卸载磁盘映像。这样就可以啦,直接将我们刚才修改好的 img 文件拿去启动就行了。
在这个版本中,只需要修改一下分区,重新安装一下 GRUB,工作就顺利完成了。这也让脚本的编写十分容易了,因此我还为它专门编写了一个自动处理的脚本。
点此展开 mbr.sh
#!/bin/sh
if [ "$#" -ne 2 ] || ! [ -f "$1" ]; then
echo "Usage: $0 old.img.gz new.img.gz" >&2
exit 1
fi
set -e
WORKDIR=$(mktemp -d)
IMGPATH=$WORKDIR/disk.img
ESPPATH=$WORKDIR/esp
mkdir -p $ESPPATH
echo 'working directory is set to '$WORKDIR
echo '\e[1;34m## decompressing gzipped disk image\e[0m'
gzip -ckd $1 > $IMGPATH
echo
echo '\e[1;34m## changing working directory to '$WORKDIR'\e[0m'
cd $WORKDIR
echo
echo '\e[1;34m## mounting disk image to loop device\e[0m'
LODEV=$(losetup -f)
losetup -P $LODEV $IMGPATH
echo
echo '\e[1;34m## copying old boot folder\e[0m'
mount ${LODEV}p1 $ESPPATH
cp -vr $ESPPATH/boot .
umount $ESPPATH
echo
echo '\e[1;34m## changing partition type to EFI\e[0m'
sfdisk --part-type $LODEV 1 ef
echo
echo '\e[1;34m## formatting boot partition with FAT16\e[0m'
mkfs.fat -n EFI -F16 ${LODEV}p1
echo
echo '\e[1;34m## mounting boot partition\e[0m'
mount ${LODEV}p1 $ESPPATH
mkdir -p $ESPPATH/EFI/BOOT
echo
echo '\e[1;34m## copying old boot directory to new partition\e[0m'
cp -vr boot $ESPPATH
echo
echo '\e[1;34m## generating GRUB file for UEFI\e[0m'
echo 'configfile ${cmdpath}/../../boot/grub/grub.cfg' > image.cfg
grub-mkstandalone -d /usr/lib/grub/x86_64-efi/ -O x86_64-efi --locales="" --themes="" --fonts="" --modules="part_msdos" -o $ESPPATH/EFI/BOOT/BOOTX64.EFI "boot/grub/grub.cfg=image.cfg"
echo
echo '\e[1;34m## generating GRUB file for MBR\e[0m'
grub-install --target=i386-pc --locales="" --themes="" --fonts="" --boot-directory=$ESPPATH/boot $LODEV
echo
echo '\e[1;34m## unmounting\e[0m'
umount $ESPPATH
losetup -d $LODEV
echo
echo '\e[1;34m## re-compressing disk image\e[0m'
gzip -ckn $IMGPATH > $2
echo
echo '\e[1;34m## cleaning up '$WORKDIR'\e[0m'
rm -rfv $WORKDIR
关于纯 UEFI 版
可能出于兼容性、洁癖或者什么其他原因,你想要一个纯粹的 UEFI 启动/GPT 分区的 OpenWrt,在这里我这里简要提一下操作过程。
首先用类似 dd if=/dev/zero of=openwrt-efi.img bs=1M count=400
的方法创建一个空白的磁盘映像,或者使用真实的磁盘。接着使用你所喜欢的分区工具,将这个磁盘分为 ESP 和系统两个分区。分为 ESP 分区和系统分区后,挂载 ESP 分区,做与上文提到的 grub-mkstandalone
相同的操作在 ESP 分区上来创建一个 GRUB 的 EFI 文件。
下载官网的 rootfs-ext4.img.gz(分区大小 256M)用 dd 部署到想要装系统的分区,你也可以用 rootfs-generic.tar.gz
解压的方式部署到这个分区里,这样分区类型就不局限于 ext4 了(但是系统里面可能没有带上相应的内核模块)。并使用官网上下载的 vmlinuz 部署到 ESP 分区内,再使用 blkid
得到系统分区的 UUID 之后自己写一个 grub.cfg 就好了。