UEFI启动背后的原理

现在的电脑基本上都是采取UEFI启动了,之前看过一些资料,大抵都讲道“现在UEFI不需要制作引导分区了,系统会自动从EFI分区下的\EFI\Boot\Bootx64.efi文件启动”,这种说法其实是以偏概全了。导致很多情况下出现了一些奇怪的问题(比如当我准备把操作系统安装到一个移动硬盘上的时候)。

究其错误的原因,从根本上来说是忽略了了主板上NVRAM(非易失性随机访问存储器,主板上自带的一小块配置存储器,不在硬盘上,你把整块硬盘都擦了它还在)中启动配置的存在。

1. 真正的UEFI启动逻辑

def 从EFI文件启动(文件路径):
根据文件路径加载EFI文件
if 开启了SecureBoot功能:
if not SecureBoot校验通过:
启动失败报错退出 # 一般可以在屏幕上看到错误信息
执行EFI # 一般来说也就从这里进入了操作系统或者Grub,所以不会返回

def 从存储设备启动(存储设备):
# 可以是是GPT分区,也可以是MBR分区
for 分区 in 存储设备:
# 分区类型在MBR/GPT的分区表中标注,其本质是个普通FAT文件系统分区
if 分区类型 == EFI系统分区:
# 注意,FAT文件系统路径不区分大小写,且由于出自Windows,所以其路径习惯用反斜杠\而非斜杠/
# 对于非x86_64类型的硬件,默认路径也有差异
if 分区根目录下存在"\EFI\Boot\Bootx64.efi"文件:
从EFI文件启动(分区索引 + 该文件在分区下路径)

def 从EFI启动项启动(启动项):
if 该启动项指向的是一个EFI文件路径:
从EFI文件启动(启动项所指文件路径)
elif 启动项指向的是一个存储设备:
从存储设备启动(启动项所指设备)

def 启动():
根据主板NVRAM中启动项配置,以及扫描的硬件(比如刚插入装机U盘等),生成启动顺序表
if 用户无操控:
for 启动项 in 启动顺序表:
从EFI启动项启动(启动项) # 成功则返回,失败可能会返回,也可能是报错退出
else:
根据启动顺序表选择界面
if 用户选择了一个启动项:
# 这对应了手动从列表中选择一个启动设备
从EFI启动项启动(启动项)
elif 用户手动选择了一个具体的EFI文件:
# 这对应了手动浏览EFI分区并选择某一个具体的文件
从EFI文件启动(文件路径)
这次启动失败了,死机?退出主界面?或者重新尝试?结果因机器而异


硬件扫描和准备
启动()

所以,总结说来,UEFI并不是单纯地找到启动设备上的EFI分区里的某固定文件路径就启动了,而是会结合NVRAM中的配置来完成整个过程。NVRAM中的启动配置包括一系列启动项和响应的预设优先顺序,Linux下可以用efibootmgr -v命令查看。它会在以下情况下发生更改:

  • 当安装操作系统完成后,操作系统会自动添加一条启动项,如ubuntu会生成一条名为ubuntu的项目并指向EFI分区的\EFI\ubuntu\shimx64.efi文件,而Windows会生成一条名为Windows Boot Manager的项目指向EFI分区的\EFI\Microsoft\Boot\bootmgfw.efi文件。
  • 在操作系统中通过efibootmgr等工具手动修改,使用grub-install命令时如果没有指定--no-nvram也会添加一个启动项
  • 在BIOS(UEFI不是BIOS,但是习惯上还是管机器的UEFI配置界面叫BIOS)中修改
  • 由于Windows用户实在太多了,有的主板生产商会自动为Windows添加启动项(避免小白搞坏配置),甚至强制将其列为第一优先级(这就很恶心了)
  • 手动从某个设备成功启动后,硬件可能会自动地将其对应的EFI文件加入启动项(因硬件而异)
  • 硬件检测到不存在的设备/文件,自动删除启动项(因硬件而异)
  • ……

总之这个NVRAM中的启动项配置的编辑策略各不相同,甚至不排除可能有的主板出于安全(恰烂钱)考虑拒绝你对它进行编辑,强制你从Windows启动,所以需要根据自己的电脑自行实验。一般来说,台式机自由度比较大,绝大多数都是允许你自行编辑的,且不会强制更改你自定义优先级;而高端或者新的笔记本也和台式机差不多,但是又老又旧的笔记本的坑就比较多了。

因此,到这里就可以解释如下现象了:

  • 为啥删除了EFI分区下的\EFI\BOOT目录也还能正常启动?

    因为NVRAM中的启动项配置直接指向\EFI\ubuntu或者\EFI\microsoft目录,不依赖于\EFI\BOOT\EFI\BOOT下的东西只在直接从该设备启动时生效,相当于作为一个后备。

  • 为啥换了主板后就只能从Windows启动了?

    NVRAM在主板上,换了后自然全没了,剩下Windows是因为前面说的有的主板为了方便小白使用自动探测Windows启动项的存在并添加之,Linux就没福气了,这可以需要通过引导盘进入Live环境后用efibootmgr修复。

  • 为什么安装Linux后安装Windows,无法进入Linux(Grub)?

    还是能进入的,只不过Windows把自己设置成了第一优先级,默认直接进Windows;所以一般先装Windows再装Linux,这样Linux把自己设置成第一优先级,可以在Grub界面二选一。不过其实这都是小问题,改一下NVRAM配置就行,并不是很多教程说的必须先装Windows,没那么死板。

2. 关于安全启动(SecureBoot)

所有教你安装Ubuntu之前关闭SecureBoot的教程可以一律拉黑

2020年了,Ubuntu它早就向微软购买了安全启动认证密钥,所以它的\EFI\ubuntu\shimx64.efi文件是可以通过机器安全启动校验的。这个EFI文件的作用就是作为一个入口跳板,通过SecureBoot后再去加载后续EFI文件完成启动操作。

3. Grub是如何被加载的

\EFI\ubuntu\shimx64.efi同目录下有一个\EFI\ubuntu\grub.cfg。内容如下

search.fs_uuid 62c89a8b-9df9-4acd-bc18-8622a8d501d0 root hd0,gpt2 
set prefix=($root)'/boot/grub'
configfile $prefix/grub.cfg

第一行的uuid是我Ubuntu系统所在分区的uuid,(似乎root后的hd0,gpt2可以省略,删掉也没影响)。找到这个分区后,根据第二第三行的配置,Grub的EFIb部件会从中读取/boot/grub/grub.cfg文件作为grub启动配置(该文件其实就是执行grub-update的输出)。

然后,就是展示你熟悉的Grub窗口,在然后根据选择的Grub启动项进入操作系统,不过这已经不是UEFI的范畴了。

4. 系统安装时候的一个天坑

想象中,最理想的形式应该是系统安装时可以让用户自主选择安装到哪个EFI分区,这对双硬盘/移动硬盘用户绝对是好东西,可以保证每个硬盘上的系统互相独立,拔掉硬盘到别的电脑上也能继续用。

但是!

Windows就不必说了,向来是帮用户做决定,直接自作主张安装到它找到的第一个EFI分区中去,不让你选择。

Ubuntu倒是有一个界面让你选择安装启动引导器的设备,不过这好像是一个陈年老bug,一直没有修复,它也和Windows一样,也是不理会选择结果,直接奔着扫描到的第一个EFI分区去了。参见ubuntuforums

安装设备选择界面

所以,如果真的想要做到双硬盘独立的双系统,你可以这么事后补救

  • 进入第二个系统
  • umount现有的/boot/efi并重新挂载上正确的分区(应该是空的)
  • 使用grub-install,重建缺失的EFI文件
  • 修改/etc/fstab更正挂载到挂载选项,否则即使会在正常启动操作系统后挂载上错误的/boot/efi目录
  • 恢复第一个操作系统的EFI分区(因为默认的安装策略修改了它)
    • 如果第一个系统是同版本ubuntu,那么需要修正旧的EFI分区下的\EFI\UBUNTU\grub.cfg文件中的uuid即可,把它改回去指向第一个戏台的根目录
    • 如果是其他的Linux系统,可以重启回第一个操作系统后使用grub-install重构它的EFI分区
    • 如果第一个系统是Windows,那么应该删除EFI分区下的整ubuntu目录和BOOT目录下的所有子文件,并把\EFI\Microsoft\Boot\bootmgfw.efi复制到\EFI\BOOT\Bootx64.efi(EFI文件系统不分文件名大小写)
  • 使用efibootmgr调整启动项目(就是修改NVRAM配置)

上面的操作都挺麻烦的,其实最简单的办法还是防止这种事情发生,原理很简单,把无关硬盘在本次装机运行周期内移除。可以在Try Ubuntu环境下执行如下操作。

  • 对于SATA类型的硬盘(设备号形如/dev/sda),方法很简单

    echo 0 | sudo tee /sys/block/<如sda>/device/delete
  • 而对于NVME类型的硬盘(设备号形如/dev/nvme0n1),就有些麻烦了

    # 查看设备的PCI路径
    realpath /sys/block/<如nvme0n1>
    # 输出结果应该大致为 /sys/devices/pci0000:00/0000:00:0e.0/nvme/nvme0/nvme0n1
    # 然后根据输出结果,将对应的NVME设备移除
    echo 1 | sudo tee /sys/devices/pci0000:00/0000:00:0e.0/remove

    或者直接一行命令

    echo 1 | sudo tee $(realpath /sys/block/nvme0n1)/../../../remove
  • 当然,如果是台式机,那还可以使用物理学方法,没那么多花里胡哨的,关机拔线就完事了

此命令会在将整个磁盘设备彻底隐藏(重启后恢复),因此当然不需要担心安装到了错误的EFI分区了。