Linux 内核 chroot 的工作原理及使用 Debian GNU/Linux Live CD 修复 GRUB

created
updated
license
CC-BY-SA-4.0

source

CONTENTS

chroot 的内核实现

Linux 内核通过虚拟文件系统(VFS)处理文件路径。对于 open("/etc/passwd", O_RDONLY) 这样的系统调用,内核需要根据进程的文件系统上下文确定根目录,再沿着目录树查找 etc 和 passwd。chroot() 通过修改进程的根目录,使后续绝对路径的解析从指定目录开始。

Linux 使用 task_struct 管理进程,其中的 fs 指针引用 fs_struct。该结构保存着进程的根目录和当前工作目录:

struct path root, pwd;

这里的 struct path 由两个成员组成:

struct path {
    struct vfsmount *mnt;
    struct dentry *dentry;
};

mnt 标识文件系统的挂载实例,dentry 标识目录项。两者结合起来,确定目录在 VFS 挂载树中的位置。例如,将同一个文件系统绑定挂载到两个不同目录后,底层文件可以相同,VFS 中对应的挂载路径却不同。通过 struct path,内核能够准确保存进程正在使用的根目录。

chroot 系统调用如何修改根目录

在 fs/open.c 中,chroot() 的系统调用入口为:

SYSCALL_DEFINE1(chroot, const char __user *, filename)

假设程序调用:

chroot("/mnt/rootfs");

内核首先通过 user_path_at() 解析 /mnt/rootfs,取得对应的 struct path。随后检查该目录的访问权限、调用进程是否具备 CAP_SYS_CHROOT 能力,以及安全模块是否允许修改根目录。

检查通过后,执行:

set_fs_root(current->fs, &path);

这里的 current 指向当前任务,current->fs 保存任务的文件系统上下文,而 path 指向刚刚解析出来的目标目录。

继续进入 fs/fs_struct.c,可以找到 set_fs_root()。其中,真正改变根目录的操作是:

old_root = fs->root;
fs->root = *path;

赋值前,内核通过 path_get() 为新路径持有引用;赋值期间使用锁保护 fs_struct;赋值完成后,再通过 path_put() 释放旧根目录的引用。

由于 struct path 包含对挂载对象和目录项的引用,新根目录会在进程后续访问文件时继续保持有效。fs->root 更新之后,VFS 就能够使用这一新的路径对象进行绝对路径查找。

2 VFS 如何使用 chroot 设置的根目录

根目录的更新最终体现在 fs/namei.c 中的路径查找过程。

Linux 使用 nameidata 保存一次路径解析的状态。对于普通绝对路径,path_init() 会调用 nd_jump_root(),后者取得当前进程的根目录,并将其设置为路径查找的起点。

这一过程中的两条关键操作是:

nd->root = fs->root;
nd->path = nd->root;

第一条语句出现在 set_root() 中,用于取得进程的根目录。第二条语句出现在 nd_jump_root() 中,用于将路径查找的当前位置移动到该根目录。之后,link_path_walk() 等函数继续处理路径分量,通过目录项查找与文件系统操作定位最终文件。

例如,假设当前系统有以下目录结构:

/
├── etc/
└── mnt/
    └── rootfs/
        ├── bin/
        ├── etc/
        │   └── passwd
        └── usr/

进程执行:

chroot /mnt/rootfs /bin/sh

以后,再打开 /etc/passwd,路径查找的起点就对应原目录树中的 /mnt/rootfs。VFS 从这个位置依次查找 etc 和 passwd,最终访问 /mnt/rootfs/etc/passwd。

如果路径包含 ..,VFS 在处理父目录遍历时还会检查根目录边界。当普通路径查找已经到达进程的根目录,继续解析父目录分量会停留在这一位置。

这使程序能够继续使用 /etc/passwd、/usr/bin 等常见绝对路径,而实际文件来自指定的根文件系统。

3 chroot、chdir 与 execve 之间的关系

chroot() 修改 fs_struct.root,但进程的当前工作目录由 fs_struct.pwd 单独保存。因此,用户空间通常在调用 chroot() 后,继续调用 chdir("/"),确保当前工作目录也进入新的根目录。

一个简化的 C 程序如下:

#include <unistd.h>
#include <stdio.h>

int main(void)
{
    if (chroot("/mnt/rootfs") != 0) {
        perror("chroot");
        return 1;
    }

    if (chdir("/") != 0) {
        perror("chdir");
        return 1;
    }

    execl("/bin/sh", "sh", (char *)NULL);

    perror("execl");
    return 1;
}

程序先通过 chroot() 修改根目录,再将当前工作目录切换到新的 /,最后执行目标根文件系统中的 /bin/sh。

执行 execl() 时,系统最终通过 execve() 装载新的程序映像。对于动态链接的 ELF 程序,内核还需要加载 ELF 指定的动态链接器,随后由动态链接器装载共享库。这些文件路径同样按照进程当前的根目录解析。

因此,假设目标根文件系统包含完整的 GNU/Linux 用户空间,程序就能够使用其中的 Bash、GNU C Library、软件包管理工具及其他应用程序。

这里需要理解 Linux 内核与用户空间的关系。chroot 中的程序继续由当前正在运行的 Linux 内核执行,所有系统调用仍然进入这个内核。目标根文件系统提供的是程序、共享库、配置文件及其他用户空间资源。

这一机制使得正在运行的 Linux 可以使用另一套兼容的 GNU/Linux 根文件系统。例如,使用 Debian GNU/Linux Live CD 启动计算机后,可以通过 chroot 执行硬盘中原有 Debian GNU/Linux 安装的软件程序,并借助这些程序修复磁盘上的启动引导配置。

为什么 chroot 需要配合 mount 使用

通过 Live CD 启动的 Linux 内核已经具备访问硬盘的能力,但原有 GNU/Linux 安装的根文件系统此时可能尚未挂载。要让 chroot 能够访问它,需要先使用 mount() 系统调用相关的用户空间工具,将磁盘文件系统接入当前 VFS 挂载树。

例如:

mount /dev/sda2 /mnt

完成后,磁盘分区 /dev/sda2 中的文件可以通过 /mnt 访问。如果执行:

chroot /mnt /bin/bash

Bash 就可以通过 / 访问这一文件系统的根目录。

然而,/dev、/proc 和 /sys 涉及正在运行的 Linux 内核提供的设备和运行信息。

/dev 包含磁盘、分区以及终端等设备节点。/proc 通过 procfs 提供进程和内核状态信息。/sys 通过 sysfs 暴露内核设备模型,并包含部分固件接口。

chroot() 自身不会自动挂载这些文件系统。因此,通常需要在进入目标根目录前进行挂载:

mount --rbind /dev /mnt/dev
mount -t proc proc /mnt/proc
mount --rbind /sys /mnt/sys
mount --bind /run /mnt/run

其中,--rbind 表示递归绑定挂载,能够将源目录及其子挂载一并映射到目标位置。例如,绑定 /dev 时可以同时保留 /dev/pts 等子挂载的访问关系。

这些操作使目标用户空间能够使用 Live 内核提供的设备节点和运行时接口,同时继续读取自身的程序与配置文件。

对于 GRUB 修复,这一组合十分重要:grub-install 需要使用原 Debian GNU/Linux 安装中的 GRUB 程序和模块,同时访问当前 Linux 内核识别出的物理磁盘、分区及 UEFI 固件接口。通过挂载原有根文件系统并进入 chroot,就能够在同一个运行环境中完成这些操作。

使用 Debian GNU/Linux Live CD 修复 GRUB

GRUB(GRand Unified Bootloader)是 GNU 项目的启动引导程序。它在 Linux 内核启动之前运行,负责读取启动配置、定位内核映像与 initramfs,并将控制权交给 Linux 内核。

在传统 BIOS 模式下,GRUB 依赖磁盘引导代码及后续核心映像完成启动。在 UEFI 模式下,固件通过 EFI System Partition(ESP)加载相应的 EFI 程序。Debian GNU/Linux 通常将 GRUB 配置保存在 /boot/grub/grub.cfg,通过 grub-install 安装引导程序,通过 update-grub 重新生成启动配置。

系统引导损坏后,Live CD 提供可运行的 Linux 内核及临时 GNU/Linux 用户空间。

将原有 Debian 根文件系统挂载后,可以通过 chroot 运行原 Debian 环境中的 GRUB 工具,恢复磁盘引导程序。

1 确认磁盘与启动模式

首先通过 Debian Live CD 或 Live USB 启动计算机,打开终端并取得 root 权限:

sudo -i

查看磁盘和文件系统:

lsblk -f
fdisk -l

假设原有 Debian GNU/Linux 安装使用如下分区布局:

分区 文件系统 用途
/dev/nvme0n1p1 FAT32 EFI System Partition
/dev/nvme0n1p2 ext4 根文件系统
/dev/nvme0n1p3 swap 交换空间

实际修复时必须使用本机磁盘设备名称,并核对分区 UUID、文件系统类型和容量。写入错误磁盘可能破坏其他系统的引导配置。

对于 UEFI 启动的安装,还应检查 Live 系统是否以 UEFI 模式启动:

test -d /sys/firmware/efi &&
    echo "UEFI" || echo "Legacy BIOS"

如果需要恢复 UEFI 固件启动项,Live 环境应通过 UEFI 模式启动,从而使 Linux 内核能够访问固件运行时服务。

2 挂载原有根文件系统

假设 Debian 根分区为 /dev/nvme0n1p2,首先执行:

mount /dev/nvme0n1p2 /mnt

挂载后检查目标目录:

ls /mnt
cat /mnt/etc/fstab

这里需要特别关注 /etc/fstab 中 /boot 和 /boot/efi 的挂载配置。某些安装使用独立的 /boot 分区,这时应先挂载该分区,再挂载 ESP。

在当前示例中,/boot 位于根文件系统内部,ESP 使用 /dev/nvme0n1p1,因此执行:

mkdir -p /mnt/boot/efi
mount /dev/nvme0n1p1 /mnt/boot/efi

挂载关系现在对应:

/mnt
  ├── etc
  ├── usr
  └── boot
       ├── grub
       ├── vmlinuz-...
       ├── initrd.img-...
       └── efi   → EFI System Partition

对于包含独立 /boot 分区的安装,正确挂载所有启动相关文件系统尤为重要。否则,后续 GRUB 安装与配置生成可能操作根文件系统中被挂载点覆盖的普通目录,造成启动文件位置错误。

3 准备 chroot 的运行环境

在进入 chroot 前,需要让目标根文件系统访问设备节点与内核接口:

mount --rbind /dev /mnt/dev
mount --make-rslave /mnt/dev

mount -t proc proc /mnt/proc

mount --rbind /sys /mnt/sys
mount --make-rslave /mnt/sys

mount --bind /run /mnt/run

--make-rslave 调整递归绑定挂载的传播关系,降低目标挂载树中的变化反向传播到 Live 环境的可能性。

对于 UEFI,还应检查 EFI 变量文件系统:

findmnt /sys/firmware/efi/efivars

如果系统确实以 UEFI 模式启动,但该文件系统尚未挂载,可以执行:

mount -t efivarfs efivarfs \
    /sys/firmware/efi/efivars

efivarfs 使用户空间能够通过 Linux 内核访问 UEFI 固件变量。GRUB 安装工具可能需要使用这一接口创建或更新 NVRAM 启动项。

随后进入目标根文件系统:

chroot /mnt /bin/bash

现在,Bash 的根目录已经指向硬盘上原有的 Debian 根文件系统。执行:

cat /etc/debian_version
ls /boot

会读取原有安装中的文件。接下来执行的 grub-install 及 update-grub 也会按照这个根目录解析程序、配置和模块路径。

4 修复 UEFI 模式下的 GRUB

在 chroot 内,先确认 GRUB 软件包状态:

dpkg -l | grep -E 'grub|shim'

在 x86-64 UEFI 环境下,通常需要 grub-efi-amd64 及相关软件包。如果目标安装缺少必要软件包,可在网络和 APT 软件源正常时进行安装:

apt update
apt install grub-efi-amd64

随后执行:

grub-install \
    --target=x86_64-efi \
    --efi-directory=/boot/efi \
    --bootloader-id=debian

--target=x86_64-efi 指定 GRUB 的目标启动平台,--efi-directory 指向已经挂载的 ESP,--bootloader-id 指定固件启动项对应的名称。

GRUB 安装程序会准备目标平台所需的引导文件,将 EFI 程序写入 ESP 的相应目录,并在固件接口可用时尝试创建或更新 NVRAM 启动项。对于启用 Secure Boot 的安装,还需要确保目标系统具有适当的签名 GRUB 与 shim 软件包,保持原有的受信任启动链。

接着重新生成 GRUB 启动配置:

update-grub

该命令通常调用 grub-mkconfig,使用 /etc/default/grub、/etc/grub.d/ 中的配置以及 /boot 内的内核映像生成 /boot/grub/grub.cfg。

可以检查:

ls -l /boot/grub/grub.cfg
ls -l /boot/efi/EFI/debian/

如果安装了 efibootmgr,还可以执行:

efibootmgr -v

检查 Debian 对应的 UEFI 启动项是否已经存在。这样便完成了 EFI 引导文件和 GRUB 配置的恢复。

5 修复传统 BIOS 模式下的 GRUB

对于传统 BIOS 启动的安装,chroot 的准备流程基本相同,但 GRUB 使用不同的目标平台和安装位置。

假设原 Debian 根分区为 /dev/sda1,启动磁盘为 /dev/sda,在 Live 环境中执行:

mount /dev/sda1 /mnt

mount --rbind /dev /mnt/dev
mount --make-rslave /mnt/dev

mount -t proc proc /mnt/proc

mount --rbind /sys /mnt/sys
mount --make-rslave /mnt/sys

mount --bind /run /mnt/run

chroot /mnt /bin/bash

如果目标安装缺少 BIOS 模式的 GRUB 软件包,可以安装:

apt install grub-pc

然后执行:

grub-install --target=i386-pc /dev/sda
update-grub

i386-pc 是 GRUB 为传统 x86 BIOS 启动平台使用的目标名称。这里指定的 /dev/sda 是整块启动磁盘,GRUB 需要据此安装磁盘引导代码和核心映像。

若磁盘使用 GPT 分区表并通过 BIOS 启动,还应检查是否存在供 GRUB 使用的 BIOS Boot Partition。GRUB 需要在适当位置存放核心映像,缺少必要空间时,安装过程可能失败。此类错误需要结合磁盘分区布局处理,不能直接依赖强制安装选项绕过检查。

6 退出 chroot 并重新启动

完成 GRUB 安装及配置生成后,退出目标环境:

exit

回到 Live 系统的 root shell 后,执行:

sync
umount -R /mnt

sync 请求将待写入的数据同步到存储设备,umount -R 递归卸载目标目录下的挂载点。如果某个挂载点仍被进程占用,应检查是否有终端或其他程序停留在 /mnt 内。

最后执行:

reboot

移除 Live 介质,让计算机从已修复的磁盘启动。

特殊文件系统布局下的处理

如果原 Debian GNU/Linux 安装使用 LUKS 加密、LVM 或 Btrfs 子卷,必须先在 Live 环境中建立正确的块设备和文件系统挂载关系,然后才能进入 chroot。

例如,使用 LUKS 加密根分区时,可以先执行:

cryptsetup luksOpen /dev/sda3 cryptroot

解锁后,若映射设备直接包含根文件系统,就可以执行:

mount /dev/mapper/cryptroot /mnt

如果 LUKS 内部使用 LVM,需要先激活卷组,再挂载包含根文件系统的逻辑卷。对于 Btrfs,则可能需要根据原安装的子卷配置,使用 subvol= 或 subvolid= 挂载选项。

这些操作的目的都是让 /mnt 对应原安装实际使用的根文件系统视图。随后挂载 /boot、ESP 和内核接口,再通过 chroot 执行 GRUB 修复。

如果 grub-install 成功但仍无法启动,还需要根据实际故障检查 initramfs、根文件系统 UUID、/etc/fstab、LUKS 配置和 GRUB 内核启动参数。例如,原安装的磁盘 UUID 发生变化时,已经安装的 GRUB 可能能够加载 Linux 内核,但内核初始化阶段仍可能无法定位根文件系统。

chroot 在 GRUB 修复中的作用

整个修复过程对应两次文件系统环境的建立。第一次通过 mount 将原 Debian 根文件系统连接到 Live 内核管理的 VFS 挂载树,使磁盘文件可以通过 /mnt 访问。第二次通过 chroot 将进程的 fs_struct.root 指向这个挂载路径,使原有 Debian 用户空间中的程序能够使用其正常的绝对路径。

之后,grub-install 使用原 Debian 的 GRUB 模块与配置,并通过 Live Linux 内核的设备接口访问实际磁盘或 ESP。update-grub 则使用原 Debian 的内核文件与配置重新生成启动菜单。

从内核实现到实际修复,关键关系始终相同:mount 将目标文件系统接入 VFS,chroot 改变进程的路径解析起点,execve 装载目标用户空间程序,而 Linux 内核继续提供执行程序所需的系统调用和设备访问能力。 这使已经无法正常启动的 GNU/Linux 安装仍然可以在 Live 环境中得到维护和修复。