本节目录
本文主要介绍遇到GRUB问题导致无法启动的解决方案。早期GRUB可能无法复制该方法。
问题描述
有一次笔者在双系统的机器上,在Windows下重新分了个区,以为只影响Windows,结果关机后试图再开机时就出现了GRUB RESCUE……后来试图直接删除grub.cfg来复现(因为笔者认为这应该是GRUB部分缺失导致的,于是试图删除一部分来复现问题),结果发现行为并不一致,但很相似。这里一并记录下来。
网络上有些教程实在是难以操作,有些就只告诉你要输入EFI所在位置作为参数,但是,我要是知道它在哪不就好了吗?谁能保证第一次遇到这个问题之前就会特意留心这件事?当然也有些教程很有启发性,可惜时隔许久,已经忘记当时是跟着哪个教程操作的了,反正肯定不是那些什么细节都略去的。
前提知识
bootloader
可以认为bootloader是UEFI完成后执行的一种简单的程序装载器。这种装载器通常无法具有很大的体量,而是用尽可能少的开销实现预定功能。它们将磁盘上指定位置的内容读取出来,装入内存并开始执行。
Linux的常见bootloader有vivi、uboot、grub等。
GRUB
要弄懂怎么修GRUB,不一定要完全了解它,但最起码不能完全不知道它是啥。
简单而言,GRUB是装载并合理配置了GRUB的计算机上,在UEFI/BIOS之后,OS的bootloader之前启动的一个东西,本质上它也是一个bootloader。它支持直接启动某个OS,也可以先启动另一个bootloader,由另一个来链式启动其他OS。尚不清楚这种链式启动是否有调用深度限制之类的。总之,GRUB提供了一个多系统计算机解决方案,允许用户通过它方便地选取不同选项。GRUB必需的数据在MBR(磁盘主引导记录)上存在一部分,普通文件系统上存在另一部分,缺一不可。
Secure Boot
Secure Boot是现代UEFI启动机制中的重要构成。简单而言,很多计算机主板内置了一个/一些公钥,bootloader等启动引导程序必须经过认证才能运行。最早,这个功能被设计用于保护计算机安全,但是从结果上看,Secure Boot经常被微软拿来做垄断市场之用。有一些机型的Secure Boot设置因为和某些厂家签了协议而不能修改,但多数硬件厂商还是良心的(至少没那么坑)。有些驱动模块也需要UEFI签名(实际上和签名可能有一定区别,我不是很确定)来解决,这项操作可以通过MOK工具进行。
内核和用户程序
内核是OS的核心部分,掌管底层的和敏感的操作。按照设计理念可大体分为两种,很多功能装进一个大内核就叫宏内核,分成许多功能专一的小内核就叫微内核。各种IO操作大多需要内核来辅助完成,获取某些系统关键信息也需要内核。很多驱动都在内核态工作。与之相对的,用户态程序需要的权限低很多,但能直接接触到的设备也少很多。绝大多数时候,用户程序要和外部设备通讯,都需要内核的辅助。很多系统调用都需要陷入内核才能完成。
大致的boot过程
首先,UEFI内部程序执行,确定启动顺序和当前启动对象合法后开始把指定的程序装载到RAM下并执行。对于GRUB而言,第一步是从MBR载入配置。然后,将MBR中指示的分区的GRUB相关文件载入(一般是某个linux卷下的/boot/grub/grub.cfg)。如果MBR载入关键文件失败,将会进入rescue模式。如果关键文件载入成功但找不到分区中的配置,将会进入GRUB normal模式的终端CLI。
找到grub.cfg后,GRUB将会载入配置,对文件中的文本执行分析(尚不清楚如果出现非法语法会怎么样,似乎没有能直接修改grub.cfg的做法。值得一提的是确实有grub-customizer这样的东西和/etc/default/grub可以自定义它),然后生成对应的菜单。一个常识是,这个菜单所依赖的配置文件并不是每次启动时实时生成的。
选择对应的菜单,或者进入GRUB终端执行对应指令后,OS才被启动。对linux系统而言,这个过程中,通常是先载入内核,再载入一个叫做initrd的东西,后者提供一个临时的可执行文件环境进行二段引导。虽然挂载信息由前者提供,但是如果挂载选项错误,实际上运行到后者才会报错。两段都成功通过才算是真正启动了OS。
一些基本操作
几乎任何linux shell里都支持Tab补全,GRUB终端也一样。GRUB终端很多指令的用法和bash一样,不过少很多。也有些完全不一样的,比如file指令。善用命令补全。
系统版本
Ubuntu 16.04 LTS,GNU GRUB version 2.02~beta2-36ubuntu3.22,内核4.15.0-72-generic
修复过程
无论是否启动了GRUB核心模块,前面的操作基本完全相同。这里以进入了grub rescue为例。
确定正确分区(必需)
首先执行ls,确定有哪些待确定的磁盘设备。接下来的任何操作都和这些设备中的某一个有关,现在需要确认它的位置。
grub rescue> ls
(hd0) (hd0,msdos5) (hd0,msdos1)
不同机器输出可能不一样,这里我是用虚拟机复现的。
我们不关心那些(hd[0-9]+)这类形式的项目,只关心形如(hd[0-9]+,msdos[0-9]+)的项目。通常,hd后的数字指示磁盘,msdos后的数字指示分区。整个括号里的字符串指示一个分区。下面以(<any partname>)指代一个任意分区。
要记得,要寻找的分区永远是linux的/boot目录所在分区。对可能是目标的分区分别执行
ls (<any partname>)
像这样
grub rescue> ls (hd0,msdos5)
Partition hd0,msdos5:No known filesystem detected - Partition start at
XXXXKiB - Total size XXXXKiB
如此尝试,直到遇到这样的输出:
grub rescue> ls (hd0,msdos1)
Partition hd0,msdos1: Filesystem type ext* - Last modification time
YYYY-MM-DD hh:mm:ss XXXXday, UUID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -
Partition start at XXXXKiB - Total size XXXXKiB
这还没完,加个斜杠再检查输出:
grub rescue> ls (hd0,msdos1)/
lost+found/ etc/ media/ bin/ boot/ dev/ home/ lib/ lib64/ mnt/ opt/ proc/ root/
run/ sbin/ snap/ srv/ sys/ tmp/ usr/ var/ initrd.img initrd.img.old vmlinuz vml
inuz.old cdrom/
如果输出与上面差不多一样(必须有/boot这个目录,别的长得像就行了),那就说明你找到了正确的分区。
我们这里将找到的分区记作<spec partname>。同时注意到执行
ls (<spec partname>)
会输出一长串字符,它就是UUID,最好用其他东西记下它。下文中这个字符串记作<spec UUID>。
设置变量(可选)
实践表明不设置没有太大问题,但稳妥起见最好设置一下。
grub rescue> set ROOT='<spec partname>'
grub rescue> set PREFIX=(<spec partname>)/boot/grub
# 例如<spec partname>值为hd0,msdos1,那么就是
# set ROOT='hd0,msdos1'
# set PREFIX=(hd0,msdos1)/boot/grub
# 没有/boot/grub的话,换成(hd0,msdos1)/boot也许能行
进入正常GRUB模式(仅限grub rescue)
如果您的命令提示符不是grub rescue而是grub,那么这步应该没用,当然执行一下也没有害处。
grub rescue> insmod normal
grub rescue> normal
如果grub.cfg没有丢失,那么此时应该已经进入正常的GRUB界面或OS。如果确实如此,就可以直接跳到最后一步。否则,还需要跟着接下来的步骤。
确定要启动的内核版本
grub> ls /boot
应该会输出一堆vmlinuz开头的文件名,例如vmlinuz-4.15.0-72-generic。这里4.15.0-72-generic就是内核版本,记作<kernelver>;多数vmlinuz文件都应该是vmlinuz-<kernelver>的形式,并且有对应的initrd.img-<kernelver>。任意选择一个<kernelver>并且保证对应版本的vmlinuz和initrd.img都存在就好。
查看UUID对应设备文件名(可选)
很多教程告诉你,要看UUID就得去/dev/disk/by-uuid,其实这是非常不负责任的。毕竟,linux指令执行后才有这个目录,但GRUB都坏了,linux指令显然还没执行。
不做这一步无伤大雅,但是做了的话比较方便。下文将找到的设备文件名记作<devfile>。
常规操作
文件系统里其实有个常驻嘉宾可以告诉我们UUID和设备文件名的对应关系。
执行
grub> cat /etc/fstab
输出大概像是这样:
grub> cat /etc/fstab
# /etc/fstab: static file system information.
# 无关内容省略
# <file system> <mount point> <type> <options> <dump> <pass>
# / was on /dev/sdAX during installation
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx / ext4 errors=remoun
t-ro 0 1
# /foo was on /dev/sdBY during installation
UUID=yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyy /foo swap sw
0 0
这里我们需要在各个UUID项目中找到值为<spec UUID>的,通常它对应根目录的装载点。运气好的话这一条不会被有限的屏幕大小刷过去,而且挂载信息上面还会有一行注释:
# / was on /dev/sdAX during installation
这个/dev/sdAX就是我们要找的设备文件。记下它,作为<devfile>。
非常规操作
如果你试图查看/etc/fstab时不幸地发现它被刷上去了或者没有注释,那么还有一根救命稻草。我们提到了/etc/disk/by-uuid在linux执行后出现,那我们就执行一下。
grub> linux /boot/vmlinuz-<kernelver> ro
grub> initrd /boot/initrd.img-<kernelver>
grub> boot
# 例如选定<kernelver>为4.15.0-72-generic时,
# linux /boot/vmlinuz-4.15.0-72-generic ro
# initrd /boot/initrd.img-4.15.0-72-generic
# boot
# 总之最好保证两个版本号一致,尚不清楚不一致的后果
# 网络上很多教程宣称要用linux16而非linux指令,实际上并不一定
然后应该会出现这个情况:
run-init: current directory on the same filesystem as the root: error 0
No init found. Try passing init= bootarg.
BusyBox v1.22.1 (Ubuntu 1:1.22.0-15Ubuntu1.4) built-in shell (ash)
Enter 'help' for a list of built-in commands.
(initramfs)
这里这个(initramfs)是ash的命令提示符。所谓的initramfs,很显然就是指初始化时(init)的一个内存(ram)文件系统(fs)。这时执行
(initramfs) ls /dev/disk/by-uuid
这时应该列出了一堆UUID,并且除非你的机器装了个磁盘阵列,否则不太可能正好把要找的UUID刷到屏幕外。找到对应的<spec UUID>后执行
(initramfs) readlink /dev/disk/by-uuid/<spec UUID>
# 例如,<spec UUID>为12345678-abcd-abcd-abcd-12345678,那就输入
# readlink /dev/disk/by-uuid/12345678-abcd-abcd-abcd-12345678
# 没有写错的话会有如下输出
../../sdAX
记录下输出的字符串,在/dev/disk/by-uuid这个基础上转化为绝对路径。例如,如果输出了…/…/sda1,那么<spec UUID>对应的<devfile>就应该是/dev/sda1。
记在其他东西上后reboot。
手动引导Linux启动
为什么不引导Windows?因为要先修好GRUB呀。
首先确定启动参数指定方式,记作<bootarg>。如果只有<spec UUID>而没有对应的设备文件(即<devfile>),那么<bootarg>就是UUID=<spec UUID>。如果有对应设备文件,<bootarg>就是<devfile>。这就是为什么要多走一步寻找<devfile>,因为只有UUID的话启动参数太长了,还很容易输错(并且不会立即报错,出错就只能reboot或reset)。
执行
grub> linux /boot/vmlinuz-<kernelver> ro root=<bootarg>
grub> initrd /boot/initrd.img-<kernelver>
grub> boot
# 举例而言,<kernelver>选定为4.15.0-72-generic,
# <spec UUID>为12345678-abcd-abcd-abcd-12345678,<devfile>为/dev/sda2,
# 那么<bootarg>就是/dev/sda2或者UUID=12345678-abcd-abcd-abcd-12345678
# 这时,下列两个指令是等价的:
# linux vmlinuz-4.15.0-72-generic ro root=UUID=12345678-abcd-abcd-abcd-12345678
# linux vmlinuz-4.15.0-72-generic ro root=/dev/sda2
# 两个执行其一。然后执行
# initrd /boot/initrd.img-4.15.0-72-generic
# boot
这样就能手动引导Linux启动了。
顺便一提,如果GRUB没问题却卡在initramfs出不去,可能是rootdelay太短了。在GRUB界面相应菜单项按e,linux指令后加一个rootdelay=90也许能解决问题。若是不想每次手动加,就修改/etc/default/grub。
修复GRUB(必需)
进入系统后,执行以下两个指令之一:
sudo update-grub
# 如果版本够新,考虑update-grub2
sudo sh -c "grub-mkconfig > /boot/grub/grub.cfg"
然后reboot检验一下,你的GRUB应该已经回来了。
正常机器进入GRUB终端
如果不想有机器崩溃的风险却希望试一试手动引导Linux,可以试试GRUB自带终端。如果不是自动显示GRUB菜单项,可以在UEFI差不多结束时(机器logo消失一般就是UEFI结束,是硬件提供商logo而不是系统logo)按住Shift进入GRUB选单。然后,按C进入GRUB终端,可以在这里按上述方法试一试,失败的话直接重启就行,没有太大的风险。但风险并不是完全没有,仍然有可能不得不硬复位导致系统关键文件损坏,但这个可能相比于实际的事故导致损坏,实在是小太多了。