从内核源码看Linux启动:chroot、execve与MS_MOVE的协奏曲

作为一名内核调试爱好者,我一直对"系统如何从initramfs无缝切换到硬盘上的systemd"这个经典话题充满好奇。为了直观观察这一过程,我修改了内核中mountmove_mountchrootexecve的系统调用实现,加入了自定义的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

上面的日志对应以下调用链:

  1. 用户态mount工具发起mount(MS_MOVE, "/dev", "/root/dev"),内核在SYSCALL_DEFINE5(mount)中识别到flags & MS_MOVE,打印来源和目标。

  2. 随后进入do_mountpath_mountdo_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)

execvefs/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_bufnew_buf来释放内存,用old_path_strnew_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 启动各类系统服务

八、结语

通过内核源码埋点,我们不仅验证了经典的启动流程,还深刻理解了chrootexecve的本质区别:一个换"视图",一个换"灵魂"。同时,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);
}
相关推荐
影视飓风TIM2 小时前
Linux下C语言缓冲区原理 + Git版本控制
linux·c语言·git
来者皆善2 小时前
ZYNQ linux上使用 USB CDC ACM
linux·运维·服务器
缓慢更新3 小时前
企业档案管理系统迁移实录:从文件服务器到智能检索引擎
运维·服务器
Dawn-bit3 小时前
Linux磁盘分区与Swap和磁盘故障查询
linux·运维·服务器·网络·云计算
梦想三三4 小时前
LangChain Output Parser 实战:从字符串到结构化数据的完整指南
android·服务器·langchain·github·uv
netho04 小时前
影刀rpa证书题库使用教学
运维·服务器·rpa
llilian_165 小时前
便携式相位测量模块应用方案的核心优势
网络·功能测试·单片机
无足鸟ICT5 小时前
【RHCA+】$[]
linux·运维·服务器
运维技术小记5 小时前
国产化环境配置 VNC 远程桌面:麒麟 V10 实战
linux·运维·服务器