作为一名内核调试爱好者,我一直对"系统如何从initramfs无缝切换到硬盘上的systemd"这个经典话题充满好奇。为了直观观察这一过程,我修改了内核中mount、move_mount、chroot和execve的系统调用实现,加入了自定义的YYM-DBG日志。最终通过journalctl捕获的日志,完整还原了启动链条。本文将结合这些日志和内核源码,深入浅出地剖析每一步,并分享调试中的有趣细节。
一、挂载真实根:ext4_fill_super 登场
当内核加载完initramfs后,首要任务是识别并挂载真正的根文件系统。日志的第一条印证了这一点:
c
YYM-DBG: [ext4_fill_super] Attempting to fill ext4 superblock for device sda2
此日志源自fs/ext4/super.c中的ext4_fill_super函数,它读取设备/dev/sda2的超级块,校验文件系统类型,并初始化相应的inode操作。这一步成功后,该分区的文件系统结构已在内存中建立,但尚未挂载到任何目录。
二、挂载点的"迁移术":MS_MOVE 与 do_move_mount
接下来,initramfs需要将内核虚拟文件系统(/dev、/proc、/sys等)以及真实根分区移动到正确的路径。这是通过带有MS_MOVE标志的mount系统调用实现的。日志中清晰可见:
text
YYM-DBG: [mount] MS_MOVE from=/dev to=/root/dev (caller=mount)
YYM-DBG: [do_move_mount] moving /dev -> /root/dev, beneath=0, caller=mount
...
YYM-DBG: [mount] MS_MOVE from=. to=/ (caller=run-init)
YYM-DBG: [do_move_mount] moving /root -> /, beneath=0, caller=run-init
上面的日志对应以下调用链:
-
用户态
mount工具发起mount(MS_MOVE, "/dev", "/root/dev"),内核在SYSCALL_DEFINE5(mount)中识别到flags & MS_MOVE,打印来源和目标。 -
随后进入
do_mount→path_mount→do_move_mount(在fs/namespace.c中)。该函数通过attach_recursive_mnt将旧挂载树摘下,重新挂接到新挂载点。
特别值得注意的是最后一条:run-init将/root移动到/。此时,真实根文件系统已被挂载到系统的VFS根目录,但当前进程(PID 1)的"根目录视图"仍然停留在initramfs ,需要通过chroot来修正。
三、切换视角:chroot 的妙用
日志紧接着出现了关键的一行:
text
YYM-DBG: [chroot] path=. (caller=run-init)
chroot系统调用在内核fs/open.c中实现,其核心代码如下(我们添加了日志):
c
SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
// ... 省略权限检查
set_fs_root(current->fs, &path); // 修改当前进程的根目录指针
// ...
}
这里的set_fs_root将当前进程的fs->root指向新路径(此处为.,即run-init的当前工作目录,已经通过chdir("/")切换到新根)。自此,该进程及其未来创建的子进程都将以新根作为/。但请注意 :chroot仅改变"地址空间"的根,程序本身仍是run-init的代码。
四、终极替换:execve 让 PID 1 涅槃重生
紧接着,日志记录:
text
YYM-DBG: [execve] file=/sbin/init (caller=run-init)
execve在fs/exec.c中实现,其核心流程为:
c
SYSCALL_DEFINE3(execve, ...)
{
return do_execve(getname(filename), argv, envp);
}
do_execve会释放当前进程的内存描述符(mm_struct),加载目标ELF文件,并设置新的入口点。关键点 :进程的task_struct、PID、父进程关系均保持不变,因此新的/sbin/init(指向/lib/systemd/systemd)直接继承了PID 1,成为系统所有进程的祖先。
至此,run-init彻底完成了使命,systemd正式接管系统。
五、调试心得:d_path 中的"四指针"艺术
在do_move_mount中添加日志时,我使用了这样的代码:
c
char *old_buf = kmalloc(PATH_MAX, GFP_KERNEL);
char *new_buf = kmalloc(PATH_MAX, GFP_KERNEL);
char *old_path_str = d_path(old_path, old_buf, PATH_MAX);
char *new_path_str = d_path(new_path, new_buf, PATH_MAX);
pr_info("moving %s -> %s", old_path_str, new_path_str);
kfree(old_buf);
kfree(new_buf);
为什么需要四个指针?因为d_path要求调用者提供缓冲区,但它返回的指针不一定等于缓冲区起始地址 (它从末尾向前填充)。我们必须用old_buf和new_buf来释放内存,用old_path_str和new_path_str来打印内容。若混用,轻则打印乱码,重则因释放错误地址导致内核崩溃。
六、海量 execve 的真相:fork + exec 模型
你可能注意到日志中后续有数百条execve,例如:
text
YYM-DBG: [execve] file=/usr/lib/systemd/systemd-journald (caller=(journald))
YYM-DBG: [execve] file=/usr/bin/mount (caller=(mount))
这些并不是 在替换PID 1,而是systemd通过fork()产生子进程,然后子进程调用execve加载各自的服务程序。父进程systemd(PID 1)始终不变,它负责管理和监控这些子进程。这就是Linux中"启动新程序"的标准范式:fork负责克隆,execve负责变身。
七、完整流程回顾
| 步骤 | 系统调用 | 内核函数 | 作用 |
|---|---|---|---|
| 1 | mount(普通) |
ext4_fill_super |
识别并初始化根分区 |
| 2 | mount(MS_MOVE) |
do_move_mount |
将虚拟文件系统移入/root |
| 3 | mount(MS_MOVE) |
do_move_mount |
将/root移到/,完成VFS切换 |
| 4 | chroot(".") |
set_fs_root |
改变当前进程的根目录视图 |
| 5 | execve("/sbin/init") |
do_execve |
替换进程映像为systemd |
| 6 | 大量 fork+execve |
copy_process+do_execve |
启动各类系统服务 |
八、结语
通过内核源码埋点,我们不仅验证了经典的启动流程,还深刻理解了chroot和execve的本质区别:一个换"视图",一个换"灵魂"。同时,d_path的调试也让我体会到内核编程对内存安全的极致要求。
##源码
cpp
SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
char __user *, type, unsigned long, flags, void __user *, data)
{
int ret;
char *kernel_type;
char *kernel_dev;
char *kernel_dir = NULL; // 新增:用于打印目标路径
void *options;
kernel_type = copy_mount_string(type);
ret = PTR_ERR(kernel_type);
if (IS_ERR(kernel_type))
goto out_type;
kernel_dev = copy_mount_string(dev_name);
ret = PTR_ERR(kernel_dev);
if (IS_ERR(kernel_dev))
goto out_dev;
options = copy_mount_options(data);
ret = PTR_ERR(options);
if (IS_ERR(options))
goto out_data;
// ========== YYM 安全打印日志 ==========
// 拷贝 dir_name 到内核空间用于打印(仅调试用途)
kernel_dir = copy_mount_string(dir_name);
if (!IS_ERR(kernel_dir)) {
// 如果 flags 包含 MS_MOVE,则这是一个移动挂载操作
if (flags & MS_MOVE) {
pr_info("YYM-DBG: [mount] MS_MOVE from=%s to=%s (caller=%s)",
kernel_dev ? kernel_dev : "(null)",
kernel_dir ? kernel_dir : "(null)",
current->comm);
} else {
// 普通挂载(可选打印,可能会刷屏,建议加条件)
pr_info("YYM-DBG: [mount] dev=%s dir=%s type=%s flags=0x%lx (caller=%s)",
kernel_dev ? kernel_dev : "(null)",
kernel_dir ? kernel_dir : "(null)",
kernel_type ? kernel_type : "(null)",
flags, current->comm);
}
kfree(kernel_dir); // 打印完立即释放
} else {
pr_info("YYM-DBG: [mount] failed to copy dir_name (caller=%s)", current->comm);
}
ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);
kfree(options);
out_data:
kfree(kernel_dev);
out_dev:
kfree(kernel_type);
out_type:
return ret;
}
long do_mount(const char *dev_name, const char __user *dir_name,
const char *type_page, unsigned long flags, void *data_page)
{
struct path path;
int ret;
//yym-gaizao
// ========== YYM 添加日志开始 ==========
pr_info("YYM-DBG: [do_mount] caller=%s\n", current->comm);
ret = user_path_at(AT_FDCWD, dir_name, LOOKUP_FOLLOW, &path);
if (ret)
return ret;
ret = path_mount(dev_name, &path, type_page, flags, data_page);
path_put(&path);
return ret;
}
static int do_move_mount(struct path *old_path, struct path *new_path,
bool beneath)
{
//yym-gaizao
// ========== YYM 添加详细日志 ==========
char *old_buf = NULL, *new_buf = NULL;
char *old_path_str = NULL, *new_path_str = NULL;
// 分配缓冲区
old_buf = kmalloc(PATH_MAX, GFP_KERNEL);
new_buf = kmalloc(PATH_MAX, GFP_KERNEL);
if (old_buf && new_buf) {
// 使用 d_path 获取路径字符串(返回指向缓冲区的指针)
old_path_str = d_path(old_path, old_buf, PATH_MAX);
new_path_str = d_path(new_path, new_buf, PATH_MAX);
if (!IS_ERR(old_path_str) && !IS_ERR(new_path_str)) {
pr_info("YYM-DBG: [do_move_mount] moving %s -> %s, beneath=%d, caller=%s",
old_path_str, new_path_str, beneath, current->comm);
} else {
pr_info("YYM-DBG: [do_move_mount] d_path failed (old_err=%ld, new_err=%ld), caller=%s",
PTR_ERR(old_path_str), PTR_ERR(new_path_str), current->comm);
}
} else {
pr_info("YYM-DBG: [do_move_mount] kmalloc failed for path buffers, caller=%s",
current->comm);
}
// 释放缓冲区(即便 kmalloc 失败,kfree(NULL) 也安全)
kfree(old_buf);
kfree(new_buf);
// ========================================
struct mnt_namespace *ns;
struct mount *p;
struct mount *old;
struct mount *parent;
struct mountpoint *mp, *old_mp;
int err;
bool attached;
enum mnt_tree_flags_t flags = 0;
mp = do_lock_mount(new_path, beneath);
if (IS_ERR(mp))
return PTR_ERR(mp);
old = real_mount(old_path->mnt);
p = real_mount(new_path->mnt);
parent = old->mnt_parent;
attached = mnt_has_parent(old);
if (attached)
flags |= MNT_TREE_MOVE;
old_mp = old->mnt_mp;
ns = old->mnt_ns;
err = -EINVAL;
/* The mountpoint must be in our namespace. */
if (!check_mnt(p))
goto out;
/* The thing moved must be mounted... */
if (!is_mounted(&old->mnt))
goto out;
/* ... and either ours or the root of anon namespace */
if (!(attached ? check_mnt(old) : is_anon_ns(ns)))
goto out;
if (old->mnt.mnt_flags & MNT_LOCKED)
goto out;
if (!path_mounted(old_path))
goto out;
if (d_is_dir(new_path->dentry) !=
d_is_dir(old_path->dentry))
goto out;
/*
* Don't move a mount residing in a shared parent.
*/
if (attached && IS_MNT_SHARED(parent))
goto out;
if (beneath) {
err = can_move_mount_beneath(old_path, new_path, mp);
if (err)
goto out;
err = -EINVAL;
p = p->mnt_parent;
flags |= MNT_TREE_BENEATH;
}
/*
* Don't move a mount tree containing unbindable mounts to a destination
* mount which is shared.
*/
if (IS_MNT_SHARED(p) && tree_contains_unbindable(old))
goto out;
err = -ELOOP;
if (!check_for_nsfs_mounts(old))
goto out;
for (; mnt_has_parent(p); p = p->mnt_parent)
if (p == old)
goto out;
err = attach_recursive_mnt(old, real_mount(new_path->mnt), mp, flags);
if (err)
goto out;
/* if the mount is moved, it should no longer be expire
* automatically */
list_del_init(&old->mnt_expire);
if (attached)
put_mountpoint(old_mp);
out:
unlock_mount(mp);
if (!err) {
if (attached)
mntput_no_expire(parent);
else
free_mnt_ns(ns);
}
return err;
}
SYSCALL_DEFINE1(chroot, const char __user *, filename)
{
struct path path;
int error;
unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
struct filename *tmp = NULL;
//yym-gaizao
// ========== YYM 添加日志开始 ==========
// 使用 getname 安全拷贝用户态路径字符串
tmp = getname(filename);
if (!IS_ERR(tmp)) {
pr_info("YYM-DBG: [chroot] path=%s (caller=%s)",
tmp->name, current->comm);
putname(tmp); // 立即释放,避免内存泄漏
} else {
pr_info("YYM-DBG: [chroot] getname failed (caller=%s)",
current->comm);
}
// ======================================
retry:
error = user_path_at(AT_FDCWD, filename, lookup_flags, &path);
if (error)
goto out;
error = path_permission(&path, MAY_EXEC | MAY_CHDIR);
if (error)
goto dput_and_out;
error = -EPERM;
if (!ns_capable(current_user_ns(), CAP_SYS_CHROOT))
goto dput_and_out;
error = security_path_chroot(&path);
if (error)
goto dput_and_out;
set_fs_root(current->fs, &path);
error = 0;
dput_and_out:
path_put(&path);
if (retry_estale(error, lookup_flags)) {
lookup_flags |= LOOKUP_REVAL;
goto retry;
}
out:
return error;
}
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
struct filename *tmp = NULL;
//yym-gaizao
// ========== YYM 添加日志开始 ==========
tmp = getname(filename);
if (!IS_ERR(tmp)) {
pr_info("YYM-DBG: [execve] file=%s (caller=%s)",
tmp->name, current->comm);
putname(tmp);
} else {
pr_info("YYM-DBG: [execve] getname failed (caller=%s)",
current->comm);
}
// ======================================
return do_execve(getname(filename), argv, envp);
}