【linux kernel】linux内核如何启动用户空间进程【2】

一、开篇

在《【linux kernel】linux内核如何启动用户空间进程-01》一文中,分析了start_kernel函数启动用户空间进程的入口点,本文将分析余下部分的启动过程。

对于在linux内核启动主线start_kernel()函数中调用的run_init_process()函数,或者是execveat系统调用,这两个操作到最后都将会调用到do_execveat_common()这个重磅函数。该函数用于linux内核执行一个新的程序。do_execveat_common()函数原型如下(定义在/fs/exec.c文件中):

static int do_execveat_common(int fd, struct filename *filename,struct user_arg_ptr argv, struct user_arg_ptr envp,int flags)

该函数有5个参数。第一个参数fd表示应用程序目录的文件描述符,AT_FDCWD表示给定的路径名是相对于调用进程的当前工作目录的。第五个参数是flags标志。

下文,就来详细分析一下该函数。

do_execveat_common()函数中,首先会检查文件名指针,如果文件名指针为NULL则返回。在此之后,将检查当前进程的标志,以确保运行进程没有超过限制,如下代码片段:

if (IS_ERR(filename))
	return PTR_ERR(filename);

if ((current->flags & PF_NPROC_EXCEEDED) &&
	atomic_read(&current_user()->processes) > rlimit(RLIMIT_NPROC)) {
	retval = -EAGAIN;
	goto out_ret;
}

current->flags &= ~PF_NPROC_EXCEEDED;

如果以上两个检查都成功了,将取消当前进程标志中的PF_NPROC_EXCEEDED标志,以防止执行失败。接下来,将调用kernel/fork.c中定义的unshare_files()函数,解引出当前任务的struct file_struct,并检查该函数结果。内核此处这样做,是为了调用unshare_files函数来消除被执行二进制文件的文件描述符的潜在泄漏。如下代码所示:

retval = unshare_files(&displaced);
if (retval)
	goto out_ret;

接着,将设置由struct linux_binprm结构表示的bprm变量的数据。linux_binprm结构用于保存linux内核加载用户空间二进制文件时使用的参数(定义在include/linux/binfmt .h头文件中),定义如下:

struct linux_binprm {
    //linux_binprm 缓存区
	char buf[BINPRM_BUF_SIZE];
#ifdef CONFIG_MMU
	struct vm_area_struct *vma;
	unsigned long vma_pages;
#else
# define MAX_ARG_PAGES	32
	struct page *page[MAX_ARG_PAGES];
#endif
    //内存管理结构
	struct mm_struct *mm;
    //当前内存顶部
	unsigned long p; /* current top of mem */
	unsigned int
		cred_prepared:1,/* true if creds already prepared (multiple
				 * preps happen for interpreters) */
		cap_effective:1;/* true if has elevated effective capabilities,
				 * false if not; except for init which inherits
				 * its parent's caps anyway */
#ifdef __alpha__
	unsigned int taso:1;
#endif
    //只有search_binary_handler ()
	unsigned int recursion_depth; 
    //file描述结构
	struct file * file;
    //表示新的cred结构
	struct cred *cred;	/* new credentials */
    //表示这个exec有多不安全(掩码为LSM_UNSAFE_*)
	int unsafe;		
	unsigned int per_clear;	/* bits to clear in current->personality */
	int argc, envc;
    //procps看到的二进制文件的名称
	const char * filename;	
    //实际执行的二进制文件的名称。
	const char * interp;	
	unsigned interp_flags;
	unsigned interp_data;
	unsigned long loader, exec;
};

do_execveat_common()函数中,会使用用kzalloc()函数为bprm结构分配内存,并检查分配结果,如下代码所示:

bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
if (!bprm)
	goto out_files;

在此之后,通过调用prepare_bprm_creds()函数来设置binprm凭据,如下代码:

retval = prepare_bprm_creds(bprm);
	if (retval)
		goto out_free;

check_unsafe_exec(bprm);
current->in_execve = 1;

可见,binprm凭据初始化的本质就是存储在linux_binprm结构中的cred结构的初始化。cred结构包含任务的安全上下文,例如:任务的真实uid、任务的真实guid、虚拟文件系统操作的uid和guid等。紧接着,将调用check_unsafe_exec()函数检查现在是否可以安全地执行程序,并将当前进程设置为in_execve状态。如下代码片段:

check_unsafe_exec(bprm);
current->in_execve = 1;

在以上操作之后,会调用do_open_execat()函数,该函数将进行以下几个步骤的操作:

(1)检查传递给do_execveat_common函数的标志

(2)搜索并打开磁盘上的可执行文件

(3)检查将从noexec挂载点加载的二进制文件(需要避免从不包含可执行二进制文件(如proc或sysfs)的文件系统中执行二进制文件)。

(4)初始化文件结构并返回指向该结构的指针。

如下代码片段:

file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
	goto out_unmark;

接着将调用sched_exec()函数,该函数用于确定能够执行新程序且负载最小的处理器,并将当前进程迁移到该处理器上。在此之后,需要检查给出可执行二进制文件的文件描述符。尝试检查二进制文件的名称是从/符号开始的,或者给定可执行二进制文件的路径是相对于调用进程的当前工作目录解析的(文件描述符fd参数是AT_FDCWD)。如果这些检查中有一个操作成功,将设置二进制参数filename,如下代码片段:

bprm->file = file;

if (fd == AT_FDCWD || filename->name[0] == '/') {
	bprm->filename = filename->name;
}

否则,如果文件名为空,则将二进制参数filename设置为/dev/fd/%d或/dev/fd/%d/%s,这一点取决于给定的可执行二进制文件的文件名,如下代码片段:

} else {
	if (filename->name[0] == '\0')
		pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
	else
		pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
		                    fd, filename->name);
	if (!pathbuf) {
		retval = -ENOMEM;
		goto out_unmark;
	}

	bprm->filename = pathbuf;
}

bprm->interp = bprm->filename;

从以上代码可见,不仅设置了bprm->filename参数,而且还设置了包含程序解释器名称的bprm->interp参数。

下一步将初始化linux_binprm结构变量bprm的其他字段。首先调用bprm_mm_init函数并将bprm传递给它,如以下代码:

retval = bprm_mm_init(bprm);
if (retval)
	goto out_unmark;

bprm_mm_init函数用于初始化bprm结构中的内存描述符,换言之,bprm_mm_init函数初始化mm_struct结构体,并用一个临时堆栈vm_area_struct填充它。mm_struct定义在(/include/linux/mm_types.h)文件中,用于表示进程的地址空间。

接着,将计算传递给可执行二进制文件的命令行参数的计数,以及环境变量的计数,并分别将其设置为到bprm->argc和bprm->envc中,如下代码所示:

bprm->argc = count(argv, MAX_ARG_STRINGS);
if ((retval = bprm->argc) < 0)
	goto out;

bprm->envc = count(envp, MAX_ARG_STRINGS);
if ((retval = bprm->envc) < 0)
	goto out;

上述代码中MAX_ARG_STRINGS是一个宏,定义在include/uapi/linux/binfmt .h头文件中,该宏表示传递给execve系统调用的最大字符串数量。MAX_ARG_STRINGS的值定义如下:

#define MAX_ARG_STRINGS 0x7FFFFFFF

计算了命令行参数和环境变量的数量之后,会调用prepare_binprm()函数,如下代码:

retval = prepare_binprm(bprm);
if (retval < 0)
	goto out;

使用来自inode的uid填充linux_binprm结构,并从二进制可执行文件读取128字节。为什么只从可执行文件中读取前128个字节呢?因为需要靠这128个字节来检查可执行文件的类型,然后将在后续步骤中读取可执行文件的剩余部分。在准备好linux_bprm结构之后,会调用copy_strings_kernel函数将可执行二进制文件的文件名、命令行参数和环境变量复制到linux_bprm中,如下代码片段:

retval = copy_strings_kernel(1, &bprm->filename, bprm);
if (retval < 0)
	goto out;

retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
	goto out;

retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
	goto out;

并将指针指向在bprm_mm_init函数中设置的新程序堆栈的顶部,如下代码:

bprm->exec = bprm->p;

堆栈的顶部将包含程序文件名,这里将这个文件名存储到了linux_bprm结构的exec字段中。

到目前为止,内核已经填充了linux_bprm结构,接下来将调用exec_binprm函数,如下代码:

retval = exec_binprm(bprm);
if (retval < 0)
	goto out;

exec_binprm()函数是linux内核启动用户空间进程的核心关键函数,接下来,我们就来看一下exec_binprm()函数的内部实现(同样定义在/fs/exec.c文件中):

首先,将当前任务命名空间中的pid和pid存储在exec_binprm中,如下代码所示:

old_pid = current->pid;
rcu_read_lock();
old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
rcu_read_unlock();

接着,调用search_binary_handler(bprm)函数,这个函数将遍历包含不同二进制格式的处理程序列表。目前linux内核支持以下几种二进制格式:

  • binfmt_script:支持从#!开始的解释脚本。

  • binfmt_misc:根据linux内核的运行时配置,支持不同的二进制格式。

  • binfmt_elf:支持elf格式。

  • binfmt_aout:支持a.out格式。

  • binfmt_flat:支持flat格式。

  • binfmt_elf_fdpic:支持elf FDPIC二进制。

  • binfmt_em86:支持在Alpha机器上运行Intel elf二进制文件。

因此,在search_binary_handler()函数中将调用load_binary()函数并将linux_binprm传递给它。如果二进制处理程序支持给定的可执行文件格式,那么将开始准备可执行的二进制文件以便后续执行,如下代码片段:

int search_binary_handler(struct linux_binprm *bprm)
{
   /* 省略了部分代码 */
	list_for_each_entry(fmt, &formats, lh) {
		retval = fmt->load_binary(bprm);
		if (retval < 0 && !bprm->mm) {
			force_sigsegv(SIGSEGV, current);
			return retval;
		}
	}
	
	return retval;	

从以上代码可知,fmt->load本质是一个函数指针,在linux内核中,对于不同的二进制格式的程序,内核都定义并实现了对应的加载函数,本文以elf格式为例。其linux_binfmt定义如下(/fs/binfmt_elf.c):

static struct linux_binfmt elf_format = {
	.module		= THIS_MODULE,
	.load_binary	= load_elf_binary,
	.load_shlib	= load_elf_library,
	.core_dump	= elf_core_dump,
	.min_coredump	= ELF_EXEC_PAGESIZE,
};

从以上代码可知,对于elf格式的文件的load_binary加载函数是load_elf_binary(),下文讲分析该函数。

在load_elf_binary()函数中会在linux_bprm缓冲区中检查magic号(每个elf二进制文件的头中都包含magic 号)(注:从前文可知,内核已经从可执行二进制文件中读取了前128字节);如果不是elf二进制格式,则退出。如下代码片段:

static int load_elf_binary(struct linux_binprm *bprm)
{
    /* 省略大部分内容 */
	loc->elf_ex = *((struct elfhdr *)bprm->buf);

	if (memcmp(elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
		goto out;

如果给定的可执行文件是elf格式,load_elf_binary()将为执行可执行程序文件做以下准备工作:

(1)检查可执行文件的架构和类型,如下代码:

if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
	goto out;
if (!elf_check_arch(&loc->elf_ex))
	goto out;

如果有错误的架构和可执行文件非可执行、非共享,则退出。

(2)如果第(1)步操作成功,将继续执行后续操作,接下来将尝试加载程序头表。如下代码片段:

elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
if (!elf_phdata)
	goto out;

(3)从磁盘读取与可执行二进制文件相链接的程序解释器和库,并将其加载到内存中。

(4)在load_elf_binary()函数的最后,会调用start_thread(),这是一个架构相关函数或宏定义(本文以ARM架构为例)。其定义如下(/arch/arm/include/asm/processor.h):

#define start_thread(regs,pc,sp)					\
({									\
	memset(regs->uregs, 0, sizeof(regs->uregs));			\
	if (current->personality & ADDR_LIMIT_32BIT)			\
		regs->ARM_cpsr = USR_MODE;				\
	else								\
		regs->ARM_cpsr = USR26_MODE;				\
	if (elf_hwcap & HWCAP_THUMB && pc & 1)				\
		regs->ARM_cpsr |= PSR_T_BIT;				\
	regs->ARM_cpsr |= PSR_ENDSTATE;					\
	regs->ARM_pc = pc & ~1;		/* pc */			\
	regs->ARM_sp = sp;		/* sp */			\
	nommu_start_thread(regs);					\
})

调用该函数时传递了三个参数给该宏定义,如下代码片段:

    start_thread(regs, elf_entry, bprm->p);
    retval = 0;
out:
    kfree(loc);
out_ret:
    return retval;

(1)regs:表示新任务的寄存器集。

(2)elf_entry:表示新任务的入口点的地址。

(3)bprm->p:表示新任务的堆栈顶部的地址。

从以上过程可见,新任务的入口地址elf_entry通过start_thread(),传递给了reg->ARM_pc,从而指定了内核待启动的用户空间进程(此处内核不会立即执行用户空间进程)。

当exec_binprm执行完后,将回到do_execveat_common()函数中,继续进行后续的操作:释放之前分配的内存空间并返回。如下代码片段:

	current->fs->in_exec = 0;
	current->in_execve = 0;
	acct_update_integrals(current);
	task_numa_free(current);
	free_bprm(bprm);
	kfree(pathbuf);
	putname(filename);
	if (displaced)
		put_files_struct(displaced);
	return retval;

至此,当从execveat系统调用处理程序返回后,内核将开始执行用户空间的程序。execve系统调用并不会将控制权返回给进程,但是调用进程的代码、数据和其他段覆盖了程序段。应用程序的退出将通过exit系统调用实现。


搜索/关注【嵌入式小生】vx公众号,获取更多精彩内容>>>>
请添加图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Linux操作系统中有两个主要的运行空间用户空间内核空间。 1. 用户空间(User Space):用户空间是操作系统中用于运行用户程序的区域。在用户空间中,用户可以执行各种应用程序,如文本编辑器、浏览器、音乐播放器等。用户空间提供了一系列的系统调用(system call)接口,允许应用程序与底层的操作系统内核进行交互。用户空间通常拥有较低的权限,不能直接访问和操作硬件资源。 2. 内核空间(Kernel Space):内核空间是操作系统内核运行的区域,它是操作系统的核心部分。在内核空间中,操作系统直接控制着硬件资源,如CPU、内存、设备驱动等。内核提供了各种系统服务,如进程管理、文件系统、网络协议栈等。与用户空间相比,内核空间拥有更高的权限,能够执行特权指令并直接访问硬件资源。 用户空间内核空间之间通过系统调用进行通信。当应用程序需要执行一些特权操作(例如读写硬件设备、创建新进程),它会通过系统调用请求内核帮助执行这些操作。内核会在接收到系统调用请求后,检查请求的合法性,并在必要执行相应的操作,然后将结果返回给用户空间用户空间内核空间的划分是为了提高系统的安全性和稳定性。通过将用户程序与操作系统内核隔离开来,可以防止恶意程序对系统的破坏,并确保操作系统的正常运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iriczhao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值